Merge "[Player] Added missing comments." into tizen
[platform/core/csapi/tizenfx.git] / src / Tizen.Multimedia.MediaPlayer / Player / Player.cs
1 /*
2  * Copyright (c) 2016 Samsung Electronics Co., Ltd All Rights Reserved
3  *
4  * Licensed under the Apache License, Version 2.0 (the License);
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an AS IS BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 using System;
17 using System.Threading.Tasks;
18 using System.Runtime.InteropServices;
19 using System.Diagnostics;
20 using System.IO;
21 using System.Threading;
22 using static Interop;
23
24 namespace Tizen.Multimedia
25 {
26     static internal class PlayerLog
27     {
28         internal const string Tag = "Tizen.Multimedia.Player";
29         internal const string Enter = "[ENTER]";
30         internal const string Leave = "[LEAVE]";
31     }
32
33     /// <summary>
34     /// Provides the ability to control media playback.
35     /// </summary>
36     /// <remarks>
37     /// The Player provides functions to play a media content.
38     /// It also provides functions to adjust the configurations of the player such as playback rate, volume, looping etc.
39     /// Note that only one video player can be played at one time.
40     /// </remarks>
41     public class Player : IDisposable, IDisplayable<PlayerErrorCode>
42     {
43         private PlayerHandle _handle;
44
45         /// <summary>
46         /// Occurs when playback of a media is finished.
47         /// </summary>
48         public event EventHandler<EventArgs> PlaybackCompleted;
49         private NativePlayer.PlaybackCompletedCallback _playbackCompletedCallback;
50
51         /// <summary>
52         /// Occurs when playback of a media is interrupted.
53         /// </summary>
54         public event EventHandler<PlaybackInterruptedEventArgs> PlaybackInterrupted;
55         private NativePlayer.PlaybackInterruptedCallback _playbackInterruptedCallback;
56
57         /// <summary>
58         /// Occurs when any error occurs.
59         /// </summary>
60         /// <remarks>The event handler will be executed on an internal thread.</remarks>
61         public event EventHandler<PlayerErrorOccurredEventArgs> ErrorOccurred;
62         private NativePlayer.PlaybackErrorCallback _playbackErrorCallback;
63
64         /// <summary>
65         /// Occurs when the video stream changed.
66         /// </summary>
67         /// <remarks>The event handler will be executed on an internal thread.</remarks>
68         public event EventHandler<VideoStreamChangedEventArgs> VideoStreamChanged;
69         private NativePlayer.VideoStreamChangedCallback _videoStreamChangedCallback;
70
71         /// <summary>
72         /// Occurs when the subtitle is updated.
73         /// </summary>
74         /// <remarks>The event handler will be executed on an internal thread.</remarks>
75         public EventHandler<SubtitleUpdatedEventArgs> SubtitleUpdated;
76         private NativePlayer.SubtitleUpdatedCallback _subtitleUpdatedCallback;
77
78         /// <summary>
79         /// Occurs when there is a change in the buffering status of streaming.
80         /// </summary>
81         public event EventHandler<BufferingProgressChangedEventArgs> BufferingProgressChanged;
82         private NativePlayer.BufferingProgressCallback _bufferingProgressCallback;
83
84         internal event EventHandler<MediaStreamBufferStatusChangedEventArgs> MediaStreamAudioBufferStatusChanged;
85         private NativePlayer.MediaStreamBufferStatusCallback _mediaStreamAudioBufferStatusChangedCallback;
86
87         internal event EventHandler<MediaStreamBufferStatusChangedEventArgs> MediaStreamVideoBufferStatusChanged;
88         private NativePlayer.MediaStreamBufferStatusCallback _mediaStreamVideoBufferStatusChangedCallback;
89
90         internal event EventHandler<MediaStreamSeekingOccurredEventArgs> MediaStreamAudioSeekingOccurred;
91         private NativePlayer.MediaStreamSeekCallback _mediaStreamAudioSeekCallback;
92
93         internal event EventHandler<MediaStreamSeekingOccurredEventArgs> MediaStreamVideoSeekingOccurred;
94         private NativePlayer.MediaStreamSeekCallback _mediaStreamVideoSeekCallback;
95
96         /// <summary>
97         /// Initialize a new instance of the Player class.
98         /// </summary>
99         public Player()
100         {
101             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
102
103             NativePlayer.Create(out _handle).ThrowIfFailed("Failed to create player");
104
105             Debug.Assert(_handle != null);
106
107             RetrieveProperties();
108
109             if (Features.IsSupported(Features.AudioEffect))
110             {
111                 _audioEffect = new AudioEffect(this);
112             }
113
114             if (Features.IsSupported(Features.RawVideo))
115             {
116                 RegisterVideoFrameDecodedCallback();
117             }
118         }
119
120         private void RetrieveProperties()
121         {
122             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
123
124             NativePlayer.GetAudioLatencyMode(Handle, out _audioLatencyMode).
125                 ThrowIfFailed("Failed to initialize the player");
126
127             NativePlayer.IsLooping(Handle, out _isLooping).ThrowIfFailed("Failed to initialize the player");
128
129             Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
130         }
131
132         private bool _callbackRegistered;
133
134         private void RegisterCallbacks()
135         {
136             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
137
138             if (_callbackRegistered)
139             {
140                 return;
141             }
142             RegisterSubtitleUpdatedCallback();
143             RegisterErrorOccurredCallback();
144             RegisterPlaybackInterruptedCallback();
145             RegisterVideoStreamChangedCallback();
146             RegisterBufferingCallback();
147             RegisterMediaStreamBufferStatusCallback();
148             RegisterMediaStreamSeekCallback();
149             RegisterPlaybackCompletedCallback();
150
151             _callbackRegistered = true;
152         }
153
154         /// <summary>
155         /// Gets the native handle of the player.
156         /// </summary>
157         /// <value>An IntPtr that contains the native handle of the player.</value>
158         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
159         public IntPtr Handle
160         {
161             get
162             {
163                 ValidateNotDisposed();
164                 return _handle.DangerousGetHandle();
165             }
166         }
167
168         internal void ValidatePlayerState(params PlayerState[] desiredStates)
169         {
170             Debug.Assert(desiredStates.Length > 0);
171
172             ValidateNotDisposed();
173
174             var curState = State;
175             if (curState.IsAnyOf(desiredStates))
176             {
177                 return;
178             }
179
180             throw new InvalidOperationException($"The player is not in a valid state. " +
181                 $"Current State : { curState }, Valid State : { string.Join(", ", desiredStates) }.");
182         }
183
184         #region Properties
185         #region Network configuration
186         private string _cookie = "";
187         private string _userAgent = "";
188
189         /// <summary>
190         /// Gets or Sets the cookie for streaming playback.
191         /// </summary>
192         /// <remarks>To set, the player must be in the <see cref="PlayerState.Idle"/> state.</remarks>
193         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
194         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
195         /// <exception cref="ArgumentNullException">The value to set is null.</exception>
196         public string Cookie
197         {
198             get
199             {
200                 Log.Info(PlayerLog.Tag, "get cookie : " + _cookie);
201                 return _cookie;
202             }
203             set
204             {
205                 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
206                 ValidatePlayerState(PlayerState.Idle);
207
208                 if (value == null)
209                 {
210                     Log.Error(PlayerLog.Tag, "cookie can't be null");
211                     throw new ArgumentNullException(nameof(value), "Cookie can't be null.");
212                 }
213
214                 NativePlayer.SetStreamingCookie(Handle, value, value.Length).
215                     ThrowIfFailed("Failed to set the cookie to the player");
216
217                 _cookie = value;
218                 Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
219             }
220         }
221
222         /// <summary>
223         /// Gets or Sets the user agent for streaming playback.
224         /// </summary>
225         /// <remarks>To set, the player must be in the <see cref="PlayerState.Idle"/> state.</remarks>
226         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
227         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
228         /// <exception cref="ArgumentNullException">The value to set is null.</exception>
229         public string UserAgent
230         {
231             get
232             {
233                 Log.Info(PlayerLog.Tag, "get useragent : " + _userAgent);
234                 return _userAgent;
235             }
236             set
237             {
238                 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
239                 ValidatePlayerState(PlayerState.Idle);
240
241                 if (value == null)
242                 {
243                     Log.Error(PlayerLog.Tag, "UserAgent can't be null");
244                     throw new ArgumentNullException(nameof(value), "UserAgent can't be null.");
245                 }
246
247                 NativePlayer.SetStreamingUserAgent(Handle, value, value.Length).
248                     ThrowIfFailed("Failed to set the user agent to the player");
249
250                 _userAgent = value;
251                 Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
252             }
253         }
254         #endregion
255
256         /// <summary>
257         /// Gets the state of the player.
258         /// </summary>
259         /// <value>The current state of the player.</value>
260         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
261         public PlayerState State
262         {
263             get
264             {
265                 ValidateNotDisposed();
266
267                 if (IsPreparing())
268                 {
269                     return PlayerState.Preparing;
270                 }
271
272                 int state = 0;
273                 NativePlayer.GetState(Handle, out state).ThrowIfFailed("Failed to retrieve the state of the player");
274
275                 Debug.Assert(Enum.IsDefined(typeof(PlayerState), state));
276
277                 return (PlayerState)state;
278             }
279         }
280
281         private AudioLatencyMode _audioLatencyMode;
282
283         /// <summary>
284         /// Gets or sets the audio latency mode.
285         /// </summary>
286         /// <value>A <see cref="AudioLatencyMode"/> that specifies the mode. The default is <see cref="AudioLatencyMode.Mid"/>.</value>
287         /// <remarks>
288         /// If the mode is <see cref="AudioLatencyMode.High"/>,
289         /// audio output interval can be increased so, it can keep more audio data to play.
290         /// But, state transition like pause or resume can be more slower than default(<see cref="AudioLatencyMode.Mid"/>).
291         /// </remarks>
292         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
293         /// <exception cref="ArgumentException">The value is not valid.</exception>
294         public AudioLatencyMode AudioLatencyMode
295         {
296             get
297             {
298                 Log.Info(PlayerLog.Tag, "get audio latency mode : " + _audioLatencyMode);
299                 return _audioLatencyMode;
300             }
301             set
302             {
303                 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
304                 ValidateNotDisposed();
305
306                 if (_audioLatencyMode == value)
307                 {
308                     return;
309                 }
310                 ValidationUtil.ValidateEnum(typeof(AudioLatencyMode), value);
311
312                 NativePlayer.SetAudioLatencyMode(Handle, value).
313                     ThrowIfFailed("Failed to set the audio latency mode of the player");
314
315                 _audioLatencyMode = value;
316             }
317         }
318
319         private bool _isLooping;
320
321         /// <summary>
322         /// Gets or sets the looping state.
323         /// </summary>
324         /// <value>true if the playback is looping; otherwise, false. The default value is false.</value>
325         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
326         public bool IsLooping
327         {
328             get
329             {
330                 Log.Info(PlayerLog.Tag, "get looping : " + _isLooping);
331                 return _isLooping;
332             }
333             set
334             {
335                 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
336                 ValidateNotDisposed();
337
338                 if (_isLooping == value)
339                 {
340                     return;
341                 }
342
343                 NativePlayer.SetLooping(Handle, value).ThrowIfFailed("Failed to set the looping state of the player");
344
345                 _isLooping = value;
346             }
347         }
348
349         #region Display methods
350         /// <summary>
351         /// Gets the display settings.
352         /// </summary>
353         /// <value>A <see cref="PlayerDisplaySettings"/> that specifies the display settings.</value>
354         public PlayerDisplaySettings DisplaySettings { get; }
355
356         private Display _display;
357
358         private PlayerErrorCode SetDisplay(Display display)
359         {
360             if (display == null)
361             {
362                 Log.Info(PlayerLog.Tag, "set display to none");
363                 return NativePlayer.SetDisplay(Handle, DisplayType.None, IntPtr.Zero);
364             }
365
366             return display.ApplyTo(this);
367         }
368
369         private void ReplaceDisplay(Display newDisplay)
370         {
371             _display?.SetOwner(null);
372             _display = newDisplay;
373             _display?.SetOwner(this);
374         }
375
376         /// <summary>
377         /// Gets or sets the display.
378         /// </summary>
379         /// <value>A <see cref="Multimedia.Display"/> that specifies the display.</value>
380         /// <remarks>The player must be in the <see cref="PlayerState.Idle"/> state.</remarks>
381         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
382         /// <exception cref="ArgumentException">The value has already been assigned to another player.</exception>
383         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
384         public Display Display
385         {
386             get
387             {
388                 return _display;
389             }
390             set
391             {
392                 ValidatePlayerState(PlayerState.Idle);
393
394                 if (value?.Owner != null)
395                 {
396                     if (ReferenceEquals(this, value.Owner))
397                     {
398                         return;
399                     }
400
401                     throw new ArgumentException("The display has already been assigned to another.");
402                 }
403                 SetDisplay(value).ThrowIfFailed("Failed to set the display to the player");
404
405                 ReplaceDisplay(value);
406             }
407         }
408
409         PlayerErrorCode IDisplayable<PlayerErrorCode>.ApplyEvasDisplay(DisplayType type, ElmSharp.EvasObject evasObject)
410         {
411             Debug.Assert(IsDisposed == false);
412
413             Debug.Assert(Enum.IsDefined(typeof(DisplayType), type));
414
415             return NativePlayer.SetDisplay(Handle, type, evasObject);
416         }
417         #endregion
418
419         private PlayerTrackInfo _audioTrack;
420
421         /// <summary>
422         /// Gets the track info for audio.
423         /// </summary>
424         /// <value>A <see cref="PlayerTrackInfo"/> for audio.</value>
425         public PlayerTrackInfo AudioTrackInfo
426         {
427             get
428             {
429                 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
430                 if (_audioTrack == null)
431                 {
432                     _audioTrack = new PlayerTrackInfo(this, StreamType.Audio);
433                 }
434                 return _audioTrack;
435             }
436         }
437
438         private PlayerTrackInfo _subtitleTrackInfo;
439
440         /// <summary>
441         /// Gets the track info for subtitle.
442         /// </summary>
443         /// <value>A <see cref="PlayerTrackInfo"/> for subtitle.</value>
444         public PlayerTrackInfo SubtitleTrackInfo
445         {
446             get
447             {
448                 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
449                 if (_subtitleTrackInfo == null)
450                 {
451                     _subtitleTrackInfo = new PlayerTrackInfo(this, StreamType.Text);
452                 }
453                 return _subtitleTrackInfo;
454             }
455         }
456
457         private StreamInfo _streamInfo;
458
459         /// <summary>
460         /// Gets the stream information.
461         /// </summary>
462         /// <value>A <see cref="StreamInfo"/> for this player.</value>
463         public StreamInfo StreamInfo
464         {
465             get
466             {
467                 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
468                 if (_streamInfo == null)
469                 {
470                     _streamInfo = new StreamInfo(this);
471                 }
472                 return _streamInfo;
473             }
474         }
475
476         private readonly AudioEffect _audioEffect;
477
478         /// <summary>
479         /// Gets the audio effect.
480         /// </summary>
481         /// <feature>http://tizen.org/feature/multimedia.custom_audio_effect</feature>
482         /// <exception cref="NotSupportedException">The required feature is not supported.</exception>
483         public AudioEffect AudioEffect
484         {
485             get
486             {
487                 if (_audioEffect == null)
488                 {
489                     throw new NotSupportedException($"The feature({Features.AudioEffect}) is not supported.");
490                 }
491
492                 return _audioEffect;
493             }
494         }
495
496         #endregion
497
498         #region Dispose support
499         private bool _disposed;
500
501         /// <summary>
502         /// Releases all resources used by the current instance.
503         /// </summary>
504         public void Dispose()
505         {
506             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
507             Dispose(true);
508         }
509
510         private void Dispose(bool disposing)
511         {
512             if (!_disposed)
513             {
514                 ReplaceDisplay(null);
515
516                 if (_source != null)
517                 {
518                     try
519                     {
520                         _source.DetachFrom(this);
521                     }
522                     catch (Exception e)
523                     {
524                         Log.Error(PlayerLog.Tag, e.ToString());
525                     }
526                 }
527                 _source = null;
528
529                 if (_handle != null)
530                 {
531                     _handle.Dispose();
532                 }
533                 _disposed = true;
534             }
535         }
536
537         internal void ValidateNotDisposed()
538         {
539             if (_disposed)
540             {
541                 Log.Warn(PlayerLog.Tag, "player was disposed");
542                 throw new ObjectDisposedException(nameof(Player));
543             }
544         }
545
546         internal bool IsDisposed => _disposed;
547         #endregion
548
549         #region Methods
550
551         /// <summary>
552         /// Gets or sets the mute state.
553         /// </summary>
554         /// <value>true if the player is muted; otherwise, false.</value>
555         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
556         public bool Muted
557         {
558             get
559             {
560                 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
561
562                 bool value = false;
563                 NativePlayer.IsMuted(Handle, out value).ThrowIfFailed("Failed to get the mute state of the player");
564
565                 Log.Info(PlayerLog.Tag, "get mute : " + value);
566
567                 return value;
568             }
569             set
570             {
571                 NativePlayer.SetMute(Handle, value).ThrowIfFailed("Failed to set the mute state of the player");
572             }
573         }
574
575         /// <summary>
576         /// Gets the streaming download Progress.
577         /// </summary>
578         /// <returns>The <see cref="DownloadProgress"/> containing current download progress.</returns>
579         /// <remarks>The player must be in the <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.</remarks>
580         /// <exception cref="InvalidOperationException">
581         ///     The player is not streaming.\n
582         ///     -or-\n
583         ///     The player is not in the valid state.
584         ///     </exception>
585         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
586         public DownloadProgress GetDownloadProgress()
587         {
588             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
589             ValidatePlayerState(PlayerState.Playing, PlayerState.Paused);
590
591             int start = 0;
592             int current = 0;
593             NativePlayer.GetStreamingDownloadProgress(Handle, out start, out current).
594                 ThrowIfFailed("Failed to get download progress");
595
596             Log.Info(PlayerLog.Tag, "get download progress : " + start + ", " + current);
597
598             return new DownloadProgress(start, current);
599         }
600
601         #region Volume
602         /// <summary>
603         /// Gets or sets the current volume.
604         /// </summary>
605         /// <remarks>Valid volume range is from 0 to 1.0, inclusive.</remarks>
606         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
607         /// <exception cref="ArgumentOutOfRangeException">
608         ///     <paramref name="value"/> is less than zero.\n
609         ///     -or-\n
610         ///     <paramref name="value"/> is greater than 1.0.
611         /// </exception>
612         public float Volume
613         {
614             get
615             {
616                 float value = 0.0F;
617                 NativePlayer.GetVolume(Handle, out value, out value).
618                     ThrowIfFailed("Failed to get the volume of the player");
619                 return value;
620             }
621             set
622             {
623                 if (value < 0F || 1.0F < value)
624                 {
625                     throw new ArgumentOutOfRangeException(nameof(value), value,
626                         $"Valid volume range is 0 <= value <= 1.0, but got { value }.");
627                 }
628
629                 NativePlayer.SetVolume(Handle, value, value).
630                     ThrowIfFailed("Failed to set the volume of the player");
631             }
632         }
633
634         #endregion
635
636         /// <summary>
637         /// Sets the subtitle path for playback.
638         /// </summary>
639         /// <remarks>Only MicroDVD/SubViewer(*.sub), SAMI(*.smi), and SubRip(*.srt) subtitle formats are supported.
640         ///     <para>The mediastorage privilege(http://tizen.org/privilege/mediastorage) must be added if any files are used to play located in the internal storage.
641         ///     The externalstorage privilege(http://tizen.org/privilege/externalstorage) must be added if any files are used to play located in the external storage.</para>
642         /// </remarks>
643         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
644         /// <exception cref="ArgumentException"><paramref name="path"/> is an empty string.</exception>
645         /// <exception cref="FileNotFoundException">The specified path does not exist.</exception>
646         /// <exception cref="ArgumentNullException">The path is null.</exception>
647         public void SetSubtitle(string path)
648         {
649             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
650             ValidateNotDisposed();
651
652             if (path == null)
653             {
654                 throw new ArgumentNullException(nameof(path));
655             }
656
657             if (path.Length == 0)
658             {
659                 throw new ArgumentException("The path is empty.", nameof(path));
660             }
661
662             if (!File.Exists(path))
663             {
664                 throw new FileNotFoundException($"The specified file does not exist.", path);
665             }
666
667             NativePlayer.SetSubtitlePath(Handle, path).
668                 ThrowIfFailed("Failed to set the subtitle path to the player");
669
670             Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
671         }
672
673         /// <summary>
674         /// Removes the subtitle path.
675         /// </summary>
676         /// <remarks>The player must be in the <see cref="PlayerState.Idle"/> state.</remarks>
677         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
678         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
679         public void ClearSubtitle()
680         {
681             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
682             ValidatePlayerState(PlayerState.Idle);
683
684             NativePlayer.SetSubtitlePath(Handle, null).
685                 ThrowIfFailed("Failed to clear the subtitle of the player");
686             Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
687         }
688
689         /// <summary>
690         /// Sets the offset for the subtitle.
691         /// </summary>
692         /// <param name="offset">The value indicating a desired offset in milliseconds.</param>
693         /// <remarks>The player must be in the <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.</remarks>
694         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
695         /// <exception cref="InvalidOperationException">
696         ///     The player is not in the valid state.\n
697         ///     -or-\n
698         ///     No subtitle is set.
699         /// </exception>
700         /// <seealso cref="SetSubtitle(string)"/>
701         public void SetSubtitleOffset(int offset)
702         {
703             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
704             ValidatePlayerState(PlayerState.Playing, PlayerState.Paused);
705
706             var err = NativePlayer.SetSubtitlePositionOffset(Handle, offset);
707
708             if (err == PlayerErrorCode.FeatureNotSupported)
709             {
710                 throw new InvalidOperationException("No subtitle set");
711             }
712
713             err.ThrowIfFailed("Failed to the subtitle offset of the player");
714             Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
715         }
716
717         private void Prepare()
718         {
719             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
720             NativePlayer.Prepare(Handle).ThrowIfFailed("Failed to prepare the player");
721         }
722
723         protected virtual void OnPreparing()
724         {
725             RegisterCallbacks();
726         }
727
728         /// <summary>
729         /// Prepares the media player for playback, asynchronously.
730         /// </summary>
731         /// <returns>A task that represents the asynchronous prepare operation.</returns>
732         /// <remarks>To prepare the player, the player must be in the <see cref="PlayerState.Idle"/> state,
733         ///     and a source must be set.</remarks>
734         /// <exception cref="InvalidOperationException">No source is set.</exception>
735         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
736         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
737         public virtual Task PrepareAsync()
738         {
739             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
740             if (_source == null)
741             {
742                 throw new InvalidOperationException("No source is set.");
743             }
744
745             ValidatePlayerState(PlayerState.Idle);
746
747             OnPreparing();
748
749             var completionSource = new TaskCompletionSource<bool>();
750
751             SetPreparing();
752
753             Task.Run(() =>
754             {
755                 try
756                 {
757                     Prepare();
758                     ClearPreparing();
759                     completionSource.SetResult(true);
760                 }
761                 catch (Exception e)
762                 {
763                     ClearPreparing();
764                     completionSource.TrySetException(e);
765                 }
766             });
767             Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
768
769             return completionSource.Task;
770         }
771
772         /// <summary>
773         /// Unprepares the player.
774         /// </summary>
775         /// <remarks>
776         ///     The most recently used source is reset and no longer associated with the player. Playback is no longer possible.
777         ///     If you want to use the player again, you have to set a source and call <see cref="PrepareAsync"/> again.
778         ///     <para>
779         ///     The player must be in the <see cref="PlayerState.Ready"/>, <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.
780         ///     It has no effect if the player is already in the <see cref="PlayerState.Idle"/> state.
781         ///     </para>
782         /// </remarks>
783         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
784         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
785         public virtual void Unprepare()
786         {
787             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
788             if (State == PlayerState.Idle)
789             {
790                 Log.Warn(PlayerLog.Tag, "idle state already");
791                 return;
792             }
793             ValidatePlayerState(PlayerState.Ready, PlayerState.Paused, PlayerState.Playing);
794
795             NativePlayer.Unprepare(Handle).ThrowIfFailed("Failed to unprepare the player");
796
797             OnUnprepared();
798         }
799
800         protected virtual void OnUnprepared()
801         {
802             _source?.DetachFrom(this);
803             _source = null;
804         }
805
806         /// <summary>
807         /// Starts or resumes playback.
808         /// </summary>
809         /// <remarks>
810         /// The player must be in the <see cref="PlayerState.Ready"/> or <see cref="PlayerState.Paused"/> state.
811         /// It has no effect if the player is already in the <see cref="PlayerState.Playing"/> state.\n
812         /// \n
813         /// Sound can be mixed with other sounds if you don't control the stream focus using <see cref="ApplyAudioStreamPolicy"/>.
814         /// </remarks>
815         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
816         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
817         /// <seealso cref="PrepareAsync"/>
818         /// <seealso cref="Stop"/>
819         /// <seealso cref="Pause"/>
820         /// <seealso cref="PlaybackCompleted"/>
821         /// <seealso cref="ApplyAudioStreamPolicy"/>
822         public virtual void Start()
823         {
824             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
825             if (State == PlayerState.Playing)
826             {
827                 Log.Warn(PlayerLog.Tag, "playing state already");
828                 return;
829             }
830             ValidatePlayerState(PlayerState.Ready, PlayerState.Paused);
831
832             NativePlayer.Start(Handle).ThrowIfFailed("Failed to start the player");
833             Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
834         }
835
836         /// <summary>
837         /// Stops playing media content.
838         /// </summary>
839         /// <remarks>
840         /// The player must be in the <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.
841         /// It has no effect if the player is already in the <see cref="PlayerState.Ready"/> state.
842         /// </remarks>
843         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
844         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
845         /// <seealso cref="Start"/>
846         /// <seealso cref="Pause"/>
847         public virtual void Stop()
848         {
849             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
850             if (State == PlayerState.Ready)
851             {
852                 Log.Warn(PlayerLog.Tag, "ready state already");
853                 return;
854             }
855             ValidatePlayerState(PlayerState.Paused, PlayerState.Playing);
856
857             NativePlayer.Stop(Handle).ThrowIfFailed("Failed to stop the player");
858             Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
859         }
860
861         /// <summary>
862         /// Pauses the player.
863         /// </summary>
864         /// <remarks>
865         /// The player must be in the <see cref="PlayerState.Playing"/> state.
866         /// It has no effect if the player is already in the <see cref="PlayerState.Paused"/> state.
867         /// </remarks>
868         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
869         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
870         /// <seealso cref="Start"/>
871         public virtual void Pause()
872         {
873             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
874             if (State == PlayerState.Paused)
875             {
876                 Log.Warn(PlayerLog.Tag, "pause state already");
877                 return;
878             }
879
880             ValidatePlayerState(PlayerState.Playing);
881
882             NativePlayer.Pause(Handle).ThrowIfFailed("Failed to pause the player");
883             Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
884         }
885
886         private MediaSource _source;
887
888         /// <summary>
889         /// Sets a media source for the player.
890         /// </summary>
891         /// <param name="source">A <see cref="MediaSource"/> that specifies the source for playback.</param>
892         /// <remarks>The player must be in the <see cref="PlayerState.Idle"/> state.</remarks>
893         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
894         /// <exception cref="InvalidOperationException">
895         ///     The player is not in the valid state.\n
896         ///     -or-\n
897         ///     It is not able to assign the source to the player.
898         ///     </exception>
899         /// <seealso cref="PrepareAsync"/>
900         public void SetSource(MediaSource source)
901         {
902             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
903             ValidatePlayerState(PlayerState.Idle);
904
905             if (source != null)
906             {
907                 source.AttachTo(this);
908             }
909
910             if (_source != null)
911             {
912                 _source.DetachFrom(this);
913             }
914
915             _source = source;
916             Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
917         }
918
919         /// <summary>
920         /// Captures a video frame asynchronously.
921         /// </summary>
922         /// <returns>A task that represents the asynchronous capture operation.</returns>
923         /// <feature>http://tizen.org/feature/multimedia.raw_video</feature>
924         /// <remarks>The player must be in the <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.</remarks>
925         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
926         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
927         /// <exception cref="NotSupportedException">The required feature is not supported.</exception>
928         public async Task<CapturedFrame> CaptureVideoAsync()
929         {
930             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
931
932             ValidationUtil.ValidateFeatureSupported(Features.RawVideo);
933
934             ValidatePlayerState(PlayerState.Playing, PlayerState.Paused);
935
936             TaskCompletionSource<CapturedFrame> t = new TaskCompletionSource<CapturedFrame>();
937
938             NativePlayer.VideoCaptureCallback cb = (data, width, height, size, _) =>
939             {
940                 Debug.Assert(size <= int.MaxValue);
941
942                 byte[] buf = new byte[size];
943                 Marshal.Copy(data, buf, 0, (int)size);
944
945                 t.TrySetResult(new CapturedFrame(buf, width, height));
946             };
947
948             using (var cbKeeper = ObjectKeeper.Get(cb))
949             {
950                 NativePlayer.CaptureVideo(Handle, cb)
951                     .ThrowIfFailed("Failed to capture the video");
952
953                 return await t.Task;
954             }
955         }
956
957         /// <summary>
958         /// Gets the play position in milliseconds.
959         /// </summary>
960         /// <remarks>The player must be in the <see cref="PlayerState.Ready"/>, <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.</remarks>
961         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
962         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
963         /// <seealso cref="SetPlayPositionAsync(int, bool)"/>
964         public int GetPlayPosition()
965         {
966             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
967             ValidatePlayerState(PlayerState.Ready, PlayerState.Paused, PlayerState.Playing);
968
969             int playPosition = 0;
970
971             NativePlayer.GetPlayPosition(Handle, out playPosition).
972                 ThrowIfFailed("Failed to get the play position of the player");
973
974             Log.Info(PlayerLog.Tag, "get play position : " + playPosition);
975
976             return playPosition;
977         }
978
979         private void SetPlayPosition(int milliseconds, bool accurate,
980             NativePlayer.SeekCompletedCallback cb)
981         {
982             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
983             var ret = NativePlayer.SetPlayPosition(Handle, milliseconds, accurate, cb, IntPtr.Zero);
984
985             //Note that we assume invalid param error is returned only when the position value is invalid.
986             if (ret == PlayerErrorCode.InvalidArgument)
987             {
988                 throw new ArgumentOutOfRangeException(nameof(milliseconds), milliseconds,
989                     "The position is not valid.");
990             }
991             if (ret != PlayerErrorCode.None)
992             {
993                 Log.Error(PlayerLog.Tag, "Failed to set play position, " + (PlayerError)ret);
994             }
995             ret.ThrowIfFailed("Failed to set play position");
996         }
997
998         /// <summary>
999         /// Sets the seek position for playback, asynchronously.
1000         /// </summary>
1001         /// <param name="position">The value indicating a desired position in milliseconds.</param>
1002         /// <param name="accurate">The value indicating whether the operation performs with accuracy.</param>
1003         /// <remarks>
1004         ///     <para>The player must be in the <see cref="PlayerState.Ready"/>, <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.</para>
1005         ///     <para>If the <paramref name="accurate"/> is true, the play position will be adjusted as the specified <paramref name="position"/> value,
1006         ///     but this might be considerably slow. If false, the play position will be a nearest keyframe position.</para>
1007         ///     </remarks>
1008         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
1009         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
1010         /// <exception cref="ArgumentOutOfRangeException">The specified position is not valid.</exception>
1011         /// <seealso cref="GetPlayPosition"/>
1012         public async Task SetPlayPositionAsync(int position, bool accurate)
1013         {
1014             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
1015             ValidatePlayerState(PlayerState.Ready, PlayerState.Playing, PlayerState.Paused);
1016
1017             var taskCompletionSource = new TaskCompletionSource<bool>();
1018
1019             bool immediateResult = _source is MediaStreamSource;
1020
1021             NativePlayer.SeekCompletedCallback cb = _ => taskCompletionSource.TrySetResult(true);
1022
1023             using (var cbKeeper = ObjectKeeper.Get(cb))
1024             {
1025                 SetPlayPosition(position, accurate, cb);
1026                 if (immediateResult)
1027                 {
1028                     taskCompletionSource.TrySetResult(true);
1029                 }
1030
1031                 await taskCompletionSource.Task;
1032             }
1033
1034             Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
1035         }
1036
1037         /// <summary>
1038         /// Sets playback rate.
1039         /// </summary>
1040         /// <param name="rate">The value for the playback rate. Valid range is -5.0 to 5.0, inclusive.</param>
1041         /// <remarks>
1042         ///     <para>The player must be in the <see cref="PlayerState.Ready"/>, <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.</para>
1043         ///     <para>The sound will be muted, when the playback rate is under 0.0 or over 2.0.</para>
1044         /// </remarks>
1045         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
1046         /// <exception cref="InvalidOperationException">
1047         ///     The player is not in the valid state.\n
1048         ///     -or-\n
1049         ///     Streaming playback.
1050         /// </exception>
1051         /// <exception cref="ArgumentOutOfRangeException">
1052         ///     <paramref name="rate"/> is less than 5.0.\n
1053         ///     -or-\n
1054         ///     <paramref name="rate"/> is greater than 5.0.\n
1055         ///     -or-\n
1056         ///     <paramref name="rate"/> is zero.
1057         /// </exception>
1058         public void SetPlaybackRate(float rate)
1059         {
1060             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
1061             if (rate < -5.0F || 5.0F < rate || rate == 0.0F)
1062             {
1063                 throw new ArgumentOutOfRangeException(nameof(rate), rate, "Valid range is -5.0 to 5.0 (except 0.0)");
1064             }
1065
1066             ValidatePlayerState(PlayerState.Ready, PlayerState.Playing, PlayerState.Paused);
1067
1068             NativePlayer.SetPlaybackRate(Handle, rate).ThrowIfFailed("Failed to set the playback rate.");
1069             Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
1070         }
1071
1072         /// <summary>
1073         /// Applies the audio stream policy.
1074         /// </summary>
1075         /// <param name="policy">The <see cref="AudioStreamPolicy"/> to apply.</param>
1076         /// <remarks>The player must be in the <see cref="PlayerState.Idle"/> state.</remarks>
1077         /// <exception cref="ObjectDisposedException">
1078         ///     The player has already been disposed of.\n
1079         ///     -or-\n
1080         ///     <paramref name="policy"/> has already been disposed of.
1081         /// </exception>
1082         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
1083         /// <exception cref="ArgumentNullException"><paramref name="policy"/> is null.</exception>
1084         public void ApplyAudioStreamPolicy(AudioStreamPolicy policy)
1085         {
1086             Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
1087             if (policy == null)
1088             {
1089                 throw new ArgumentNullException(nameof(policy));
1090             }
1091
1092             if (policy.Handle == IntPtr.Zero)
1093             {
1094                 throw new ObjectDisposedException(nameof(policy));
1095             }
1096
1097             ValidatePlayerState(PlayerState.Idle);
1098
1099             NativePlayer.SetAudioPolicyInfo(Handle, policy.Handle).
1100                 ThrowIfFailed("Failed to set the audio stream policy to the player");
1101         }
1102         #endregion
1103
1104         #region Callback registrations
1105         private void RegisterSubtitleUpdatedCallback()
1106         {
1107             _subtitleUpdatedCallback = (duration, text, _) =>
1108             {
1109                 Log.Debug(PlayerLog.Tag, "duration : " + duration + ", text : " + text);
1110                 SubtitleUpdated?.Invoke(this, new SubtitleUpdatedEventArgs(duration, text));
1111             };
1112
1113             NativePlayer.SetSubtitleUpdatedCb(Handle, _subtitleUpdatedCallback).
1114                 ThrowIfFailed("Failed to initialize the player");
1115         }
1116
1117         private void RegisterPlaybackCompletedCallback()
1118         {
1119             _playbackCompletedCallback = _ =>
1120             {
1121                 Log.Debug(PlayerLog.Tag, "completed callback");
1122                 PlaybackCompleted?.Invoke(this, EventArgs.Empty);
1123             };
1124             NativePlayer.SetCompletedCb(Handle, _playbackCompletedCallback).
1125                 ThrowIfFailed("Failed to set PlaybackCompleted");
1126         }
1127
1128         private void RegisterPlaybackInterruptedCallback()
1129         {
1130             _playbackInterruptedCallback = (code, _) =>
1131             {
1132                 if (!Enum.IsDefined(typeof(PlaybackInterruptionReason), code))
1133                 {
1134                     return;
1135                 }
1136
1137                 if (code == PlaybackInterruptionReason.ResourceConflict)
1138                 {
1139                     OnUnprepared();
1140                 }
1141
1142                 Log.Warn(PlayerLog.Tag, "interrupted reason : " + code);
1143                 PlaybackInterrupted?.Invoke(this, new PlaybackInterruptedEventArgs(code));
1144             };
1145
1146             NativePlayer.SetInterruptedCb(Handle, _playbackInterruptedCallback).
1147                 ThrowIfFailed("Failed to set PlaybackInterrupted");
1148         }
1149
1150         private void RegisterErrorOccurredCallback()
1151         {
1152             _playbackErrorCallback = (code, _) =>
1153             {
1154                 //TODO handle service disconnected error.
1155                 Log.Warn(PlayerLog.Tag, "error code : " + code);
1156                 ErrorOccurred?.Invoke(this, new PlayerErrorOccurredEventArgs((PlayerError)code));
1157             };
1158
1159             NativePlayer.SetErrorCb(Handle, _playbackErrorCallback).
1160                 ThrowIfFailed("Failed to set PlaybackError");
1161         }
1162
1163         #region VideoFrameDecoded event
1164
1165         private EventHandler<VideoFrameDecodedEventArgs> _videoFrameDecoded;
1166
1167         private NativePlayer.VideoFrameDecodedCallback _videoFrameDecodedCallback;
1168
1169         /// <summary>
1170         /// Occurs when a video frame is decoded.
1171         /// </summary>
1172         /// <remarks>
1173         ///     <para>The event handler will be executed on an internal thread.</para>
1174         ///     <para>The <see cref="VideoFrameDecodedEventArgs.Packet"/> in event args should be disposed after use.</para>
1175         /// </remarks>
1176         /// <feature>http://tizen.org/feature/multimedia.raw_video</feature>
1177         /// <exception cref="NotSupportedException">The required feature is not supported.</exception>
1178         /// <seealso cref="VideoFrameDecodedEventArgs.Packet"/>
1179         public event EventHandler<VideoFrameDecodedEventArgs> VideoFrameDecoded
1180         {
1181             add
1182             {
1183                 ValidationUtil.ValidateFeatureSupported(Features.RawVideo);
1184
1185                 _videoFrameDecoded += value;
1186             }
1187             remove
1188             {
1189                 ValidationUtil.ValidateFeatureSupported(Features.RawVideo);
1190
1191                 _videoFrameDecoded -= value;
1192             }
1193         }
1194
1195         private void RegisterVideoFrameDecodedCallback()
1196         {
1197             _videoFrameDecodedCallback = (packetHandle, _) =>
1198             {
1199                 var handler = _videoFrameDecoded;
1200                 if (handler != null)
1201                 {
1202                     Log.Debug(PlayerLog.Tag, "packet : " + packetHandle);
1203                     handler.Invoke(this,
1204                         new VideoFrameDecodedEventArgs(MediaPacket.From(packetHandle)));
1205                 }
1206                 else
1207                 {
1208                     MediaPacket.From(packetHandle).Dispose();
1209                 }
1210             };
1211
1212             NativePlayer.SetVideoFrameDecodedCb(Handle, _videoFrameDecodedCallback).
1213                 ThrowIfFailed("Failed to register the VideoFrameDecoded");
1214         }
1215         #endregion
1216
1217         private void RegisterVideoStreamChangedCallback()
1218         {
1219             ValidatePlayerState(PlayerState.Idle);
1220
1221             _videoStreamChangedCallback = (width, height, fps, bitrate, _) =>
1222             {
1223                 Log.Debug(PlayerLog.Tag, "height : " + height + ", width : " + width
1224                 + ", fps : " + fps + ", bitrate : " + bitrate);
1225
1226                 VideoStreamChanged?.Invoke(this, new VideoStreamChangedEventArgs(height, width, fps, bitrate));
1227             };
1228
1229             NativePlayer.SetVideoStreamChangedCb(Handle, _videoStreamChangedCallback).
1230                 ThrowIfFailed("Failed to set the video stream changed callback");
1231         }
1232
1233         private void RegisterBufferingCallback()
1234         {
1235             _bufferingProgressCallback = (percent, _) =>
1236             {
1237                 Log.Debug(PlayerLog.Tag, $"Buffering callback with percent { percent }");
1238                 BufferingProgressChanged?.Invoke(this, new BufferingProgressChangedEventArgs(percent));
1239             };
1240
1241             NativePlayer.SetBufferingCb(Handle, _bufferingProgressCallback).
1242                 ThrowIfFailed("Failed to set BufferingProgress");
1243         }
1244
1245         private void RegisterMediaStreamBufferStatusCallback()
1246         {
1247             _mediaStreamAudioBufferStatusChangedCallback = (status, _) =>
1248             {
1249                 Debug.Assert(Enum.IsDefined(typeof(MediaStreamBufferStatus), status));
1250                 Log.Debug(PlayerLog.Tag, "audio buffer status : " + status);
1251                 MediaStreamAudioBufferStatusChanged?.Invoke(this,
1252                     new MediaStreamBufferStatusChangedEventArgs(status));
1253             };
1254             _mediaStreamVideoBufferStatusChangedCallback = (status, _) =>
1255             {
1256                 Debug.Assert(Enum.IsDefined(typeof(MediaStreamBufferStatus), status));
1257                 Log.Debug(PlayerLog.Tag, "video buffer status : " + status);
1258                 MediaStreamVideoBufferStatusChanged?.Invoke(this,
1259                     new MediaStreamBufferStatusChangedEventArgs(status));
1260             };
1261
1262             RegisterMediaStreamBufferStatusCallback(StreamType.Audio, _mediaStreamAudioBufferStatusChangedCallback);
1263             RegisterMediaStreamBufferStatusCallback(StreamType.Video, _mediaStreamVideoBufferStatusChangedCallback);
1264         }
1265
1266         private void RegisterMediaStreamBufferStatusCallback(StreamType streamType,
1267             NativePlayer.MediaStreamBufferStatusCallback cb)
1268         {
1269             NativePlayer.SetMediaStreamBufferStatusCb(Handle, streamType, cb).
1270                 ThrowIfFailed("Failed to SetMediaStreamBufferStatus");
1271         }
1272
1273         private void RegisterMediaStreamSeekCallback()
1274         {
1275             _mediaStreamAudioSeekCallback = (offset, _) =>
1276             {
1277                 Log.Debug(PlayerLog.Tag, "audio seeking offset : " + offset);
1278                 MediaStreamAudioSeekingOccurred?.Invoke(this, new MediaStreamSeekingOccurredEventArgs(offset));
1279             };
1280             _mediaStreamVideoSeekCallback = (offset, _) =>
1281             {
1282                 Log.Debug(PlayerLog.Tag, "video seeking offset : " + offset);
1283                 MediaStreamVideoSeekingOccurred?.Invoke(this, new MediaStreamSeekingOccurredEventArgs(offset));
1284             };
1285
1286             RegisterMediaStreamSeekCallback(StreamType.Audio, _mediaStreamAudioSeekCallback);
1287             RegisterMediaStreamSeekCallback(StreamType.Video, _mediaStreamVideoSeekCallback);
1288         }
1289
1290         private void RegisterMediaStreamSeekCallback(StreamType streamType, NativePlayer.MediaStreamSeekCallback cb)
1291         {
1292             NativePlayer.SetMediaStreamSeekCb(Handle, streamType, cb).
1293                 ThrowIfFailed("Failed to SetMediaStreamSeek");
1294         }
1295         #endregion
1296
1297         #region Preparing state
1298
1299         private int _isPreparing;
1300
1301         private bool IsPreparing()
1302         {
1303             return Interlocked.CompareExchange(ref _isPreparing, 1, 1) == 1;
1304         }
1305
1306         private void SetPreparing()
1307         {
1308             Interlocked.Exchange(ref _isPreparing, 1);
1309         }
1310
1311         private void ClearPreparing()
1312         {
1313             Interlocked.Exchange(ref _isPreparing, 0);
1314         }
1315
1316         #endregion
1317     }
1318 }