/* * Copyright (c) 2018 Samsung Electronics Co., Ltd All Rights Reserved * * Licensed under the Apache License, Version 2.0 (the License); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an AS IS BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using static Interop; using System; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; namespace Tizen.Multimedia { internal static class PlayerLog { internal const string Tag = "Tizen.Multimedia.Player"; } /// /// Provides the ability to control media playback. /// /// /// The player provides functions to play a media content. /// It also provides functions to adjust the configurations of the player such as playback rate, volume, looping etc. /// Note that only one video player can be played at one time. /// public partial class Player : IDisposable, IDisplayable { private readonly PlayerHandle _handle; /// /// Initializes a new instance of the class. /// /// 3 public Player() { NativePlayer.Create(out _handle).ThrowIfFailed(null, "Failed to create player"); Debug.Assert(_handle != null); Initialize(); } /// /// Initializes a new instance of the class with a native handle. /// The class takes care of the life cycle of the handle. /// Thus, it should not be closed/destroyed in another location. /// /// The handle for the media player. /// The handle for occuring error. /// /// This supports the product infrastructure and is not intended to be used directly from application code. /// [EditorBrowsable(EditorBrowsableState.Never)] protected Player(IntPtr handle, Action errorHandler) { // This constructor is to support TV product player. // Be careful with 'handle'. It must be wrapped in safe handle, first. _handle = handle != IntPtr.Zero ? new PlayerHandle(handle) : throw new ArgumentException("Handle is invalid.", nameof(handle)); _errorHandler = errorHandler; } private bool _initialized; /// /// This supports the product infrastructure and is not intended to be used directly from application code. /// [EditorBrowsable(EditorBrowsableState.Never)] protected void Initialize() { if (_initialized) { throw new InvalidOperationException("It has already been initialized."); } if (Features.IsSupported(PlayerFeatures.AudioEffect)) { _audioEffect = new AudioEffect(this); } if (Features.IsSupported(PlayerFeatures.RawVideo)) { RegisterVideoFrameDecodedCallback(); } RegisterEvents(); _displaySettings = PlayerDisplaySettings.Create(this); _initialized = true; } internal void ValidatePlayerState(params PlayerState[] desiredStates) { Debug.Assert(desiredStates.Length > 0); ValidateNotDisposed(); var curState = State; if (curState.IsAnyOf(desiredStates)) { return; } throw new InvalidOperationException($"The player is not in a valid state. " + $"Current State : { curState }, Valid State : { string.Join(", ", desiredStates) }."); } #region Dispose support private bool _disposed; /// /// Releases all resources used by the current instance. /// /// 3 public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases the unmanaged resources used by the . /// /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. /// [EditorBrowsable(EditorBrowsableState.Never)] protected virtual void Dispose(bool disposing) { if (_disposed) { return; } if (disposing) { ReplaceDisplay(null); if (_source != null) { try { _source.DetachFrom(this); _source = null; } catch (Exception e) { Log.Error(PlayerLog.Tag, e.ToString()); } } if (_handle != null) { _handle.Dispose(); _disposed = true; } } } internal void ValidateNotDisposed() { if (_disposed) { Log.Warn(PlayerLog.Tag, "player was disposed"); throw new ObjectDisposedException(nameof(Player)); } } internal bool IsDisposed => _disposed; #endregion #region Methods /// /// Gets the streaming download progress. /// /// The containing current download progress. /// The player must be in the , , /// or state. /// /// The player is not streaming.
/// -or-
/// The player is not in the valid state. ///
/// The player has already been disposed of. /// 3 public DownloadProgress GetDownloadProgress() { ValidatePlayerState(PlayerState.Ready, PlayerState.Playing, PlayerState.Paused); int start = 0; int current = 0; NativePlayer.GetStreamingDownloadProgress(Handle, out start, out current). ThrowIfFailed(this, "Failed to get download progress"); Log.Info(PlayerLog.Tag, $"get download progress : {start}, {current}"); return new DownloadProgress(start, current); } /// /// Sets the subtitle path for playback. /// /// The absolute path of the subtitle file, it can be NULL in the state. /// Only MicroDVD/SubViewer(*.sub), SAMI(*.smi), and SubRip(*.srt) subtitle formats are supported. /// The mediastorage privilege(http://tizen.org/privilege/mediastorage) must be added if any files are used to play located in the internal storage. /// The externalstorage privilege(http://tizen.org/privilege/externalstorage) must be added if any files are used to play located in the external storage. /// /// The player has already been disposed of. /// is an empty string. /// The specified path does not exist. /// is null. /// 3 public void SetSubtitle(string path) { ValidateNotDisposed(); if (path == null) { throw new ArgumentNullException(nameof(path)); } if (path.Length == 0) { throw new ArgumentException("The path is empty.", nameof(path)); } if (!File.Exists(path)) { throw new FileNotFoundException($"The specified file does not exist.", path); } NativePlayer.SetSubtitlePath(Handle, path). ThrowIfFailed(this, "Failed to set the subtitle path to the player"); } /// /// Removes the subtitle path. /// /// The player must be in the state. /// The player has already been disposed of. /// The player is not in the valid state. /// 3 public void ClearSubtitle() { ValidatePlayerState(PlayerState.Idle); NativePlayer.SetSubtitlePath(Handle, null). ThrowIfFailed(this, "Failed to clear the subtitle of the player"); } /// /// Sets the offset for the subtitle. /// /// The value indicating a desired offset in milliseconds. /// The player must be in the or state. /// The player has already been disposed of. /// /// The player is not in the valid state.
/// -or-
/// No subtitle is set. ///
/// /// 3 public void SetSubtitleOffset(int offset) { ValidatePlayerState(PlayerState.Playing, PlayerState.Paused); var err = NativePlayer.SetSubtitlePositionOffset(Handle, offset); if (err == PlayerErrorCode.FeatureNotSupported) { throw new InvalidOperationException("No subtitle set"); } err.ThrowIfFailed(this, "Failed to the subtitle offset of the player"); } private void Prepare() { NativePlayer.Prepare(Handle).ThrowIfFailed(this, "Failed to prepare the player"); } /// /// Called when the is invoked. /// /// 3 protected virtual void OnPreparing() { } /// /// Prepares the media player for playback, asynchronously. /// /// A task that represents the asynchronous prepare operation. /// To prepare the player, the player must be in the state, /// and a source must be set. /// No source is set. /// The player has already been disposed of. /// The player is not in the valid state. /// /// 3 public virtual Task PrepareAsync() { if (_source == null) { throw new InvalidOperationException("No source is set."); } ValidatePlayerState(PlayerState.Idle); OnPreparing(); SetPreparing(); return Task.Factory.StartNew(() => { try { Prepare(); } finally { ClearPreparing(); } }, CancellationToken.None, TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning, TaskScheduler.Default); } /// /// Prepares the cancellable media player for playback, asynchronously. /// /// The cancellation token to cancel preparing. /// /// A task that represents the asynchronous prepare operation. /// To prepare the player, the player must be in the state, /// and a source must be set. /// The state must be to cancel preparing. /// When preparing is cancelled, a state will be changed to from . /// The player has already been disposed of. /// /// Operation failed; internal error. /// -or-
/// The player is not in the valid state. ///
/// /// /// 6 public virtual async Task PrepareAsync(CancellationToken cancellationToken) { ValidateNotDisposed(); var taskCompletionSource = new TaskCompletionSource(); if (_source == null) { throw new InvalidOperationException("No source is set."); } ValidatePlayerState(PlayerState.Idle); OnPreparing(); SetPreparing(); // register a callback to handle cancellation token anytime it occurs cancellationToken.Register(() => { ValidatePlayerState(PlayerState.Preparing); // a user can get the state before finally block is called. ClearPreparing(); Log.Warn(PlayerLog.Tag, $"preparing will be cancelled."); NativePlayer.Unprepare(Handle).ThrowIfFailed(this, "Failed to unprepare the player"); taskCompletionSource.TrySetCanceled(); }); _prepareCallback = _ => { Log.Warn(PlayerLog.Tag, $"prepared callback is called."); taskCompletionSource.TrySetResult(true); }; try { NativePlayer.PrepareAsync(Handle, _prepareCallback, IntPtr.Zero). ThrowIfFailed(this, "Failed to prepare the player"); await taskCompletionSource.Task.ConfigureAwait(false); } finally { ClearPreparing(); } } /// /// Unprepares the player. /// /// /// The most recently used source is reset and is no longer associated with the player. Playback is no longer possible. /// If you want to use the player again, you have to set a source and call again. /// /// The player must be in the , , or state. /// It has no effect if the player is already in the state. /// /// /// The player has already been disposed of. /// The player is not in the valid state. /// 3 public virtual void Unprepare() { if (State == PlayerState.Idle) { Log.Warn(PlayerLog.Tag, "idle state already"); return; } ValidatePlayerState(PlayerState.Ready, PlayerState.Paused, PlayerState.Playing); NativePlayer.Unprepare(Handle).ThrowIfFailed(this, "Failed to unprepare the player"); OnUnprepared(); } /// /// Called after the is unprepared. /// /// /// 3 protected virtual void OnUnprepared() { _source?.DetachFrom(this); _source = null; } /// /// Starts or resumes playback. /// /// /// Sound can be mixed with other sounds if you don't control the stream focus using .
/// Before Tizen 5.0, The player must be in the or state. /// It has no effect if the player is already in the state. /// Since Tizen 5.0, The player must be in the , , /// or state.
/// In case of HTTP streaming playback, the player could be internally paused for buffering. /// If the application calls this function during the buffering, the playback will be resumed by force /// and the buffering message posting by will be stopped.
/// In other cases, the player will keep playing without returning error.
///
/// The player has already been disposed of. /// The player is not in the valid state. /// /// /// /// /// /// /// 3 public virtual void Start() { ValidatePlayerState(PlayerState.Ready, PlayerState.Paused, PlayerState.Playing); NativePlayer.Start(Handle).ThrowIfFailed(this, "Failed to start the player"); } /// /// Stops playing the media content. /// /// /// The player must be in the or state. /// It has no effect if the player is already in the state. /// /// The player has already been disposed of. /// The player is not in the valid state. /// /// /// 3 public virtual void Stop() { if (State == PlayerState.Ready) { Log.Warn(PlayerLog.Tag, "ready state already"); return; } ValidatePlayerState(PlayerState.Paused, PlayerState.Playing); NativePlayer.Stop(Handle).ThrowIfFailed(this, "Failed to stop the player"); } /// /// Pauses the player. /// /// /// The player must be in the state. /// It has no effect if the player is already in the state. /// /// The player has already been disposed of. /// The player is not in the valid state. /// /// 3 public virtual void Pause() { if (State == PlayerState.Paused) { Log.Warn(PlayerLog.Tag, "pause state already"); return; } ValidatePlayerState(PlayerState.Playing); NativePlayer.Pause(Handle).ThrowIfFailed(this, "Failed to pause the player"); } private MediaSource _source; /// /// Determines whether MediaSource has set. /// This supports the product infrastructure and is not intended to be used directly from application code. /// [EditorBrowsable(EditorBrowsableState.Never)] protected bool HasSource => _source != null; /// /// Sets a media source for the player. /// /// A that specifies the source for playback. /// The player must be in the state. /// The player has already been disposed of. /// /// The player is not in the valid state.
/// -or-
/// It is not able to assign the source to the player. ///
/// /// 3 public void SetSource(MediaSource source) { ValidatePlayerState(PlayerState.Idle); if (source != null) { source.AttachTo(this); } if (_source != null) { _source.DetachFrom(this); } _source = source; } /// /// Captures a video frame, asynchronously. /// /// A task that represents the asynchronous capture operation. /// http://tizen.org/feature/multimedia.raw_video /// The player must be in the or state. /// The player has already been disposed of. /// The player is not in the valid state. /// The required feature is not supported. /// 3 public async Task CaptureVideoAsync() { ValidationUtil.ValidateFeatureSupported(PlayerFeatures.RawVideo); ValidatePlayerState(PlayerState.Playing, PlayerState.Paused); TaskCompletionSource t = new TaskCompletionSource(); NativePlayer.VideoCaptureCallback cb = (data, width, height, size, _) => { Debug.Assert(size <= int.MaxValue); byte[] buf = new byte[size]; Marshal.Copy(data, buf, 0, (int)size); t.TrySetResult(new CapturedFrame(buf, width, height)); }; using (var cbKeeper = ObjectKeeper.Get(cb)) { NativePlayer.CaptureVideo(Handle, cb) .ThrowIfFailed(this, "Failed to capture the video"); return await t.Task; } } /// /// Gets the play position in milliseconds. /// /// The current position in milliseconds. /// The player must be in the , , /// or state. /// The player has already been disposed of. /// The player is not in the valid state. /// /// /// /// 3 public int GetPlayPosition() { ValidatePlayerState(PlayerState.Ready, PlayerState.Paused, PlayerState.Playing); int playPosition = 0; NativePlayer.GetPlayPosition(Handle, out playPosition). ThrowIfFailed(this, "Failed to get the play position of the player"); Log.Info(PlayerLog.Tag, $"get play position : {playPosition}"); return playPosition; } private void NativeSetPlayPosition(long position, bool accurate, bool nanoseconds, NativePlayer.SeekCompletedCallback cb) { //Check if it is nanoseconds or milliseconds. var ret = !nanoseconds ? NativePlayer.SetPlayPosition(Handle, (int)position, accurate, cb, IntPtr.Zero) : NativePlayer.SetPlayPositionNanoseconds(Handle, position, accurate, cb, IntPtr.Zero); //Note that we assume invalid param error is returned only when the position value is invalid. if (ret == PlayerErrorCode.InvalidArgument) { throw new ArgumentOutOfRangeException(nameof(position), position, "The position is not valid."); } if (ret != PlayerErrorCode.None) { Log.Error(PlayerLog.Tag, "Failed to set play position, " + (PlayerError)ret); } ret.ThrowIfFailed(this, "Failed to set play position"); } private async Task SetPlayPosition(long position, bool accurate, bool nanoseconds) { var taskCompletionSource = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); bool immediateResult = _source is MediaStreamSource; NativePlayer.SeekCompletedCallback cb = _ => taskCompletionSource.TrySetResult(true); using (var cbKeeper = ObjectKeeper.Get(cb)) { NativeSetPlayPosition(position, accurate, nanoseconds, immediateResult ? null : cb); if (immediateResult) { taskCompletionSource.TrySetResult(true); } await taskCompletionSource.Task; } } /// /// Sets the seek position for playback, asynchronously. /// /// The value indicating a desired position in milliseconds. /// The value indicating whether the operation performs with accuracy. /// A task that represents the asynchronous operation. /// /// The player must be in the , , /// or state. /// If the is true, the play position will be adjusted as the specified value, /// but this might be considerably slow. If false, the play position will be a nearest keyframe position. /// /// The player has already been disposed of. /// The player is not in the valid state.
/// -or-
/// In case of non-seekable content, the player will return error and keep playing without changing the play position.
/// The specified position is not valid. /// /// /// /// 3 public async Task SetPlayPositionAsync(int position, bool accurate) { ValidatePlayerState(PlayerState.Ready, PlayerState.Playing, PlayerState.Paused); await SetPlayPosition(position, accurate, false); } /// /// Gets the play position in nanoseconds. /// /// The current position in nanoseconds. /// The player must be in the , , /// or state. /// The player has already been disposed of. /// The player is not in the valid state. /// /// /// /// 5 public long GetPlayPositionNanoseconds() { ValidatePlayerState(PlayerState.Ready, PlayerState.Paused, PlayerState.Playing); NativePlayer.GetPlayPositionNanoseconds(Handle, out long playPosition). ThrowIfFailed(this, "Failed to get the play position(nsec) of the player"); Log.Info(PlayerLog.Tag, $"get play position(nsec) : {playPosition}"); return playPosition; } /// /// Sets the seek position in nanoseconds for playback, asynchronously. /// /// The value indicating a desired position in nanoseconds. /// The value indicating whether the operation performs with accuracy. /// A task that represents the asynchronous operation. /// /// The player must be in the , , /// or state. /// If the is true, the play position will be adjusted as the specified value, /// but this might be considerably slow. If false, the play position will be a nearest keyframe position. /// /// The player has already been disposed of. /// The player is not in the valid state.
/// -or-
/// In case of non-seekable content, the player will return error and keep playing without changing the play position.
/// The specified position is not valid. /// /// /// /// 5 public async Task SetPlayPositionNanosecondsAsync(long position, bool accurate) { ValidatePlayerState(PlayerState.Ready, PlayerState.Playing, PlayerState.Paused); await SetPlayPosition(position, accurate, true); } /// /// Sets the playback rate. /// /// The value for the playback rate. Valid range is -5.0 to 5.0, inclusive. /// /// The player must be in the , , /// or state. /// The sound will be muted, when the playback rate is under 0.0 or over 2.0. /// /// The player has already been disposed of. /// /// The player is not in the valid state.
/// -or-
/// Streaming playback. ///
/// /// is less than -5.0.
/// -or-
/// is greater than 5.0.
/// -or-
/// is zero. ///
/// 3 public void SetPlaybackRate(float rate) { if (rate < -5.0F || 5.0F < rate || rate == 0.0F) { throw new ArgumentOutOfRangeException(nameof(rate), rate, "Valid range is -5.0 to 5.0 (except 0.0)"); } ValidatePlayerState(PlayerState.Ready, PlayerState.Playing, PlayerState.Paused); NativePlayer.SetPlaybackRate(Handle, rate).ThrowIfFailed(this, "Failed to set the playback rate."); } /// /// Applies the audio stream policy. /// /// The to apply. /// /// The player must be in the state.
///
/// does not support all .
/// Supported types are , , /// , , /// , , /// and . ///
/// /// The player has already been disposed of.
/// -or-
/// has already been disposed of. ///
/// The player is not in the valid state. /// is null. /// /// The required feature is not supported.
/// -or-
/// of is not supported on the current platform. ///
/// /// http://tizen.org/feature/multimedia.player.stream_info /// 3 public void ApplyAudioStreamPolicy(AudioStreamPolicy policy) { ValidationUtil.ValidateFeatureSupported("http://tizen.org/feature/multimedia.player.stream_info"); if (policy == null) { throw new ArgumentNullException(nameof(policy)); } ValidatePlayerState(PlayerState.Idle); var ret = NativePlayer.SetAudioPolicyInfo(Handle, policy.Handle); if (ret == PlayerErrorCode.InvalidArgument) { throw new NotSupportedException("The specified policy is not supported on the current system."); } ret.ThrowIfFailed(this, "Failed to set the audio stream policy to the player"); } /// /// Set the relative ROI (Region Of Interest) area as a decimal fraction based on the video source. /// It can be regarded as zooming operation because the specified video area will be rendered to fit to the display. /// /// The containing the ROI area information. /// /// This function requires the ratio of the each coordinate and size to the video resolution size /// to guarantee of showing the same area for the dynamic resolution video content. /// This function have to be called after setting /// /// The player has already been disposed of. /// /// Operation failed; internal error. /// -or-
/// The is not set to . ///
/// /// is less than 0.0 or greater than 1.0.
/// -or-
/// is less than 0.0 or greater than 1.0.
/// -or-
/// is less than or equal to 0.0 or greater than 1.0.
/// -or-
/// is less than or equal to 0.0 or greater than 1.0. ///
/// /// /// /// /// 5 public void SetVideoRoi(ScaleRectangle scaleRectangle) { ValidateNotDisposed(); if (scaleRectangle.ScaleX < 0 || scaleRectangle.ScaleX > 1) { throw new ArgumentOutOfRangeException(nameof(scaleRectangle.ScaleX), scaleRectangle.ScaleX, "Valid range is 0 to 1.0"); } if (scaleRectangle.ScaleY < 0 || scaleRectangle.ScaleY > 1) { throw new ArgumentOutOfRangeException(nameof(scaleRectangle.ScaleY), scaleRectangle.ScaleY, "Valid range is 0 to 1.0"); } if (scaleRectangle.ScaleWidth <= 0 || scaleRectangle.ScaleWidth > 1) { throw new ArgumentOutOfRangeException(nameof(scaleRectangle.ScaleWidth), scaleRectangle.ScaleWidth, "Valid range is 0 to 1.0 (except 0.0)"); } if (scaleRectangle.ScaleHeight <= 0 || scaleRectangle.ScaleHeight > 1) { throw new ArgumentOutOfRangeException(nameof(scaleRectangle.ScaleHeight), scaleRectangle.ScaleHeight, "Valid range is 0 to 1.0 (except 0.0)"); } NativePlayer.SetVideoRoi(Handle, scaleRectangle.ScaleX, scaleRectangle.ScaleY, scaleRectangle.ScaleWidth, scaleRectangle.ScaleHeight).ThrowIfFailed(this, "Failed to set the video roi area."); } /// /// Get the relative ROI (Region Of Interest) area as a decimal fraction based on the video source. /// /// The containing the ROI area information. /// The specified ROI area is valid only in . /// The player has already been disposed of. /// /// Operation failed; internal error. /// /// /// /// /// 5 public ScaleRectangle GetVideoRoi() { ValidateNotDisposed(); NativePlayer.GetVideoRoi(Handle, out var scaleX, out var scaleY, out var scaleWidth, out var scaleHeight).ThrowIfFailed(this, "Failed to get the video roi area"); return new ScaleRectangle(scaleX, scaleY, scaleWidth, scaleHeight); } /// /// This supports the product infrastructure and is not intended to be used directly from application code. /// [EditorBrowsable(EditorBrowsableState.Never)] protected MediaPacket GetMediaPacket(IntPtr handle) { MediaPacket mediaPacket = handle != IntPtr.Zero ? MediaPacket.From(handle) : throw new ArgumentException("MediaPacket handle is invalid.", nameof(handle)); return mediaPacket; } #endregion #region Preparing state private int _isPreparing; private bool IsPreparing() { return Interlocked.CompareExchange(ref _isPreparing, 1, 1) == 1; } /// /// This supports the product infrastructure and is not intended to be used directly from application code. /// [EditorBrowsable(EditorBrowsableState.Never)] protected void SetPreparing() { Interlocked.Exchange(ref _isPreparing, 1); } /// /// This supports the product infrastructure and is not intended to be used directly from application code. /// [EditorBrowsable(EditorBrowsableState.Never)] protected void ClearPreparing() { Interlocked.Exchange(ref _isPreparing, 0); } #endregion /// /// Enable to decode an audio data for exporting PCM from a data. /// /// The media format handle required to audio PCM specification. /// The format has to include , /// and . /// If the format is NULL, the original PCM format or platform default PCM format will be applied. /// The audio extract option. /// The player must be in the state. /// A event is called in a separate thread(not in the main loop). /// The audio PCM data can be retrieved using a event as a media packet /// and it is available until it's destroyed by . /// The packet has to be destroyed as quickly as possible after rendering the data /// and all the packets have to be destroyed before is called. /// The player has already been disposed of. /// The value is not valid. /// /// Operation failed; internal error. /// -or-
/// The player is not in the valid state. ///
/// /// /// 6 public void EnableExportingAudioData(AudioMediaFormat format, PlayerAudioExtractOption option) { ValidatePlayerState(PlayerState.Idle); ValidationUtil.ValidateEnum(typeof(PlayerAudioExtractOption), option, nameof(option)); _audioFrameDecodedCallback = (IntPtr packetHandle, IntPtr userData) => { var handler = AudioDataDecoded; if (handler != null) { Log.Debug(PlayerLog.Tag, "packet : " + packetHandle.ToString()); handler.Invoke(this, new AudioDataDecodedEventArgs(MediaPacket.From(packetHandle))); } else { MediaPacket.From(packetHandle).Dispose(); } }; NativePlayer.SetAudioFrameDecodedCb(Handle, format == null ? IntPtr.Zero : format.AsNativeHandle(), option, _audioFrameDecodedCallback, IntPtr.Zero).ThrowIfFailed(this, "Failed to register the _audioFrameDecoded"); } /// /// Disable to decode an audio data. /// /// The player must be in the or /// state. /// The player has already been disposed of. /// The player is not in the valid state. /// /// 6 public void DisableExportingAudioData() { ValidatePlayerState(PlayerState.Idle, PlayerState.Ready); NativePlayer.UnsetAudioFrameDecodedCb(Handle). ThrowIfFailed(this, "Failed to unset the AudioFrameDecoded"); _audioFrameDecodedCallback = null; } } }