c1b35a0519140eec9473dbb3e96f72e93af0cb90
[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     internal static class PlayerLog
27     {
28         internal const string Tag = "Tizen.Multimedia.Player";
29     }
30
31     /// <summary>
32     /// Provides the ability to control media playback.
33     /// </summary>
34     /// <remarks>
35     /// The player provides functions to play a media content.
36     /// It also provides functions to adjust the configurations of the player such as playback rate, volume, looping etc.
37     /// Note that only one video player can be played at one time.
38     /// </remarks>
39     public partial class Player : IDisposable, IDisplayable<PlayerErrorCode>
40     {
41         private PlayerHandle _handle;
42
43         /// <summary>
44         /// Initializes a new instance of the <see cref="Player"/> class.
45         /// </summary>
46         public Player()
47         {
48             NativePlayer.Create(out _handle).ThrowIfFailed("Failed to create player");
49
50             Debug.Assert(_handle != null);
51
52             RetrieveProperties();
53
54             if (Features.IsSupported(Features.AudioEffect))
55             {
56                 _audioEffect = new AudioEffect(this);
57             }
58
59             if (Features.IsSupported(Features.RawVideo))
60             {
61                 RegisterVideoFrameDecodedCallback();
62             }
63
64             DisplaySettings = PlayerDisplaySettings.Create(this);
65         }
66
67         internal void ValidatePlayerState(params PlayerState[] desiredStates)
68         {
69             Debug.Assert(desiredStates.Length > 0);
70
71             ValidateNotDisposed();
72
73             var curState = State;
74             if (curState.IsAnyOf(desiredStates))
75             {
76                 return;
77             }
78
79             throw new InvalidOperationException($"The player is not in a valid state. " +
80                 $"Current State : { curState }, Valid State : { string.Join(", ", desiredStates) }.");
81         }
82
83         #region Dispose support
84         private bool _disposed;
85
86         /// <summary>
87         /// Releases all resources used by the current instance.
88         /// </summary>
89         public void Dispose()
90         {
91             Dispose(true);
92         }
93
94         private void Dispose(bool disposing)
95         {
96             if (!_disposed)
97             {
98                 ReplaceDisplay(null);
99
100                 if (_source != null)
101                 {
102                     try
103                     {
104                         _source.DetachFrom(this);
105                     }
106                     catch (Exception e)
107                     {
108                         Log.Error(PlayerLog.Tag, e.ToString());
109                     }
110                 }
111                 _source = null;
112
113                 if (_handle != null)
114                 {
115                     _handle.Dispose();
116                 }
117                 _disposed = true;
118             }
119         }
120
121         internal void ValidateNotDisposed()
122         {
123             if (_disposed)
124             {
125                 Log.Warn(PlayerLog.Tag, "player was disposed");
126                 throw new ObjectDisposedException(nameof(Player));
127             }
128         }
129
130         internal bool IsDisposed => _disposed;
131         #endregion
132
133         #region Methods
134
135         /// <summary>
136         /// Gets the streaming download progress.
137         /// </summary>
138         /// <returns>The <see cref="DownloadProgress"/> containing current download progress.</returns>
139         /// <remarks>The player must be in the <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.</remarks>
140         /// <exception cref="InvalidOperationException">
141         ///     The player is not streaming.<br/>
142         ///     -or-<br/>
143         ///     The player is not in the valid state.
144         ///     </exception>
145         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
146         public DownloadProgress GetDownloadProgress()
147         {
148             ValidatePlayerState(PlayerState.Playing, PlayerState.Paused);
149
150             int start = 0;
151             int current = 0;
152             NativePlayer.GetStreamingDownloadProgress(Handle, out start, out current).
153                 ThrowIfFailed("Failed to get download progress");
154
155             Log.Info(PlayerLog.Tag, "get download progress : " + start + ", " + current);
156
157             return new DownloadProgress(start, current);
158         }
159
160         /// <summary>
161         /// Sets the subtitle path for playback.
162         /// </summary>
163         /// <remarks>Only MicroDVD/SubViewer(*.sub), SAMI(*.smi), and SubRip(*.srt) subtitle formats are supported.
164         ///     <para>The mediastorage privilege(http://tizen.org/privilege/mediastorage) must be added if any files are used to play located in the internal storage.
165         ///     The externalstorage privilege(http://tizen.org/privilege/externalstorage) must be added if any files are used to play located in the external storage.</para>
166         /// </remarks>
167         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
168         /// <exception cref="ArgumentException"><paramref name="path"/> is an empty string.</exception>
169         /// <exception cref="FileNotFoundException">The specified path does not exist.</exception>
170         /// <exception cref="ArgumentNullException">The path is null.</exception>
171         public void SetSubtitle(string path)
172         {
173             ValidateNotDisposed();
174
175             if (path == null)
176             {
177                 throw new ArgumentNullException(nameof(path));
178             }
179
180             if (path.Length == 0)
181             {
182                 throw new ArgumentException("The path is empty.", nameof(path));
183             }
184
185             if (!File.Exists(path))
186             {
187                 throw new FileNotFoundException($"The specified file does not exist.", path);
188             }
189
190             NativePlayer.SetSubtitlePath(Handle, path).
191                 ThrowIfFailed("Failed to set the subtitle path to the player");
192         }
193
194         /// <summary>
195         /// Removes the subtitle path.
196         /// </summary>
197         /// <remarks>The player must be in the <see cref="PlayerState.Idle"/> state.</remarks>
198         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
199         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
200         public void ClearSubtitle()
201         {
202             ValidatePlayerState(PlayerState.Idle);
203
204             NativePlayer.SetSubtitlePath(Handle, null).
205                 ThrowIfFailed("Failed to clear the subtitle of the player");
206         }
207
208         /// <summary>
209         /// Sets the offset for the subtitle.
210         /// </summary>
211         /// <param name="offset">The value indicating a desired offset in milliseconds.</param>
212         /// <remarks>The player must be in the <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.</remarks>
213         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
214         /// <exception cref="InvalidOperationException">
215         ///     The player is not in the valid state.<br/>
216         ///     -or-<br/>
217         ///     No subtitle is set.
218         /// </exception>
219         /// <seealso cref="SetSubtitle(string)"/>
220         public void SetSubtitleOffset(int offset)
221         {
222             ValidatePlayerState(PlayerState.Playing, PlayerState.Paused);
223
224             var err = NativePlayer.SetSubtitlePositionOffset(Handle, offset);
225
226             if (err == PlayerErrorCode.FeatureNotSupported)
227             {
228                 throw new InvalidOperationException("No subtitle set");
229             }
230
231             err.ThrowIfFailed("Failed to the subtitle offset of the player");
232         }
233
234         private void Prepare()
235         {
236             NativePlayer.Prepare(Handle).ThrowIfFailed("Failed to prepare the player");
237         }
238
239         /// <summary>
240         /// Called when the <see cref="Prepare"/> is invoked.
241         /// </summary>
242         protected virtual void OnPreparing()
243         {
244             RegisterEvents();
245         }
246
247         /// <summary>
248         /// Prepares the media player for playback, asynchronously.
249         /// </summary>
250         /// <returns>A task that represents the asynchronous prepare operation.</returns>
251         /// <remarks>To prepare the player, the player must be in the <see cref="PlayerState.Idle"/> state,
252         ///     and a source must be set.</remarks>
253         /// <exception cref="InvalidOperationException">No source is set.</exception>
254         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
255         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
256         public virtual Task PrepareAsync()
257         {
258             if (_source == null)
259             {
260                 throw new InvalidOperationException("No source is set.");
261             }
262
263             ValidatePlayerState(PlayerState.Idle);
264
265             OnPreparing();
266
267             var completionSource = new TaskCompletionSource<bool>();
268
269             SetPreparing();
270
271             Task.Run(() =>
272             {
273                 try
274                 {
275                     Prepare();
276                     ClearPreparing();
277                     completionSource.SetResult(true);
278                 }
279                 catch (Exception e)
280                 {
281                     ClearPreparing();
282                     completionSource.TrySetException(e);
283                 }
284             });
285
286             return completionSource.Task;
287         }
288
289         /// <summary>
290         /// Unprepares the player.
291         /// </summary>
292         /// <remarks>
293         ///     The most recently used source is reset and is no longer associated with the player. Playback is no longer possible.
294         ///     If you want to use the player again, you have to set a source and call <see cref="PrepareAsync"/> again.
295         ///     <para>
296         ///     The player must be in the <see cref="PlayerState.Ready"/>, <see cref="PlayerState.Playing"/>, or <see cref="PlayerState.Paused"/> state.
297         ///     It has no effect if the player is already in the <see cref="PlayerState.Idle"/> state.
298         ///     </para>
299         /// </remarks>
300         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
301         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
302         public virtual void Unprepare()
303         {
304             if (State == PlayerState.Idle)
305             {
306                 Log.Warn(PlayerLog.Tag, "idle state already");
307                 return;
308             }
309             ValidatePlayerState(PlayerState.Ready, PlayerState.Paused, PlayerState.Playing);
310
311             NativePlayer.Unprepare(Handle).ThrowIfFailed("Failed to unprepare the player");
312
313             OnUnprepared();
314         }
315
316         /// <summary>
317         /// Called after the <see cref="Player"/> is unprepared.
318         /// </summary>
319         /// <seealso cref="Unprepare"/>
320         protected virtual void OnUnprepared()
321         {
322             _source?.DetachFrom(this);
323             _source = null;
324         }
325
326         /// <summary>
327         /// Starts or resumes playback.
328         /// </summary>
329         /// <remarks>
330         /// The player must be in the <see cref="PlayerState.Ready"/> or <see cref="PlayerState.Paused"/> state.
331         /// It has no effect if the player is already in the <see cref="PlayerState.Playing"/> state.<br/>
332         /// <br/>
333         /// Sound can be mixed with other sounds if you don't control the stream focus using <see cref="ApplyAudioStreamPolicy"/>.
334         /// </remarks>
335         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
336         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
337         /// <seealso cref="PrepareAsync"/>
338         /// <seealso cref="Stop"/>
339         /// <seealso cref="Pause"/>
340         /// <seealso cref="PlaybackCompleted"/>
341         /// <seealso cref="ApplyAudioStreamPolicy"/>
342         public virtual void Start()
343         {
344             if (State == PlayerState.Playing)
345             {
346                 Log.Warn(PlayerLog.Tag, "playing state already");
347                 return;
348             }
349             ValidatePlayerState(PlayerState.Ready, PlayerState.Paused);
350
351             NativePlayer.Start(Handle).ThrowIfFailed("Failed to start the player");
352         }
353
354         /// <summary>
355         /// Stops playing the media content.
356         /// </summary>
357         /// <remarks>
358         /// The player must be in the <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.
359         /// It has no effect if the player is already in the <see cref="PlayerState.Ready"/> state.
360         /// </remarks>
361         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
362         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
363         /// <seealso cref="Start"/>
364         /// <seealso cref="Pause"/>
365         public virtual void Stop()
366         {
367             if (State == PlayerState.Ready)
368             {
369                 Log.Warn(PlayerLog.Tag, "ready state already");
370                 return;
371             }
372             ValidatePlayerState(PlayerState.Paused, PlayerState.Playing);
373
374             NativePlayer.Stop(Handle).ThrowIfFailed("Failed to stop the player");
375         }
376
377         /// <summary>
378         /// Pauses the player.
379         /// </summary>
380         /// <remarks>
381         /// The player must be in the <see cref="PlayerState.Playing"/> state.
382         /// It has no effect if the player is already in the <see cref="PlayerState.Paused"/> state.
383         /// </remarks>
384         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
385         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
386         /// <seealso cref="Start"/>
387         public virtual void Pause()
388         {
389             if (State == PlayerState.Paused)
390             {
391                 Log.Warn(PlayerLog.Tag, "pause state already");
392                 return;
393             }
394
395             ValidatePlayerState(PlayerState.Playing);
396
397             NativePlayer.Pause(Handle).ThrowIfFailed("Failed to pause the player");
398         }
399
400         private MediaSource _source;
401
402         /// <summary>
403         /// Sets a media source for the player.
404         /// </summary>
405         /// <param name="source">A <see cref="MediaSource"/> that specifies the source for playback.</param>
406         /// <remarks>The player must be in the <see cref="PlayerState.Idle"/> state.</remarks>
407         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
408         /// <exception cref="InvalidOperationException">
409         ///     The player is not in the valid state.<br/>
410         ///     -or-<br/>
411         ///     It is not able to assign the source to the player.
412         ///     </exception>
413         /// <seealso cref="PrepareAsync"/>
414         public void SetSource(MediaSource source)
415         {
416             ValidatePlayerState(PlayerState.Idle);
417
418             if (source != null)
419             {
420                 source.AttachTo(this);
421             }
422
423             if (_source != null)
424             {
425                 _source.DetachFrom(this);
426             }
427
428             _source = source;
429         }
430
431         /// <summary>
432         /// Captures a video frame, asynchronously.
433         /// </summary>
434         /// <returns>A task that represents the asynchronous capture operation.</returns>
435         /// <feature>http://tizen.org/feature/multimedia.raw_video</feature>
436         /// <remarks>The player must be in the <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.</remarks>
437         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
438         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
439         /// <exception cref="NotSupportedException">The required feature is not supported.</exception>
440         public async Task<CapturedFrame> CaptureVideoAsync()
441         {
442             ValidationUtil.ValidateFeatureSupported(Features.RawVideo);
443
444             ValidatePlayerState(PlayerState.Playing, PlayerState.Paused);
445
446             TaskCompletionSource<CapturedFrame> t = new TaskCompletionSource<CapturedFrame>();
447
448             NativePlayer.VideoCaptureCallback cb = (data, width, height, size, _) =>
449             {
450                 Debug.Assert(size <= int.MaxValue);
451
452                 byte[] buf = new byte[size];
453                 Marshal.Copy(data, buf, 0, (int)size);
454
455                 t.TrySetResult(new CapturedFrame(buf, width, height));
456             };
457
458             using (var cbKeeper = ObjectKeeper.Get(cb))
459             {
460                 NativePlayer.CaptureVideo(Handle, cb)
461                     .ThrowIfFailed("Failed to capture the video");
462
463                 return await t.Task;
464             }
465         }
466
467         /// <summary>
468         /// Gets the play position in milliseconds.
469         /// </summary>
470         /// <remarks>The player must be in the <see cref="PlayerState.Ready"/>, <see cref="PlayerState.Playing"/>,
471         /// or <see cref="PlayerState.Paused"/> state.</remarks>
472         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
473         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
474         /// <seealso cref="SetPlayPositionAsync(int, bool)"/>
475         public int GetPlayPosition()
476         {
477             ValidatePlayerState(PlayerState.Ready, PlayerState.Paused, PlayerState.Playing);
478
479             int playPosition = 0;
480
481             NativePlayer.GetPlayPosition(Handle, out playPosition).
482                 ThrowIfFailed("Failed to get the play position of the player");
483
484             Log.Info(PlayerLog.Tag, "get play position : " + playPosition);
485
486             return playPosition;
487         }
488
489         private void SetPlayPosition(int milliseconds, bool accurate,
490             NativePlayer.SeekCompletedCallback cb)
491         {
492             var ret = NativePlayer.SetPlayPosition(Handle, milliseconds, accurate, cb, IntPtr.Zero);
493
494             //Note that we assume invalid param error is returned only when the position value is invalid.
495             if (ret == PlayerErrorCode.InvalidArgument)
496             {
497                 throw new ArgumentOutOfRangeException(nameof(milliseconds), milliseconds,
498                     "The position is not valid.");
499             }
500             if (ret != PlayerErrorCode.None)
501             {
502                 Log.Error(PlayerLog.Tag, "Failed to set play position, " + (PlayerError)ret);
503             }
504             ret.ThrowIfFailed("Failed to set play position");
505         }
506
507         /// <summary>
508         /// Sets the seek position for playback, asynchronously.
509         /// </summary>
510         /// <param name="position">The value indicating a desired position in milliseconds.</param>
511         /// <param name="accurate">The value indicating whether the operation performs with accuracy.</param>
512         /// <remarks>
513         ///     <para>The player must be in the <see cref="PlayerState.Ready"/>, <see cref="PlayerState.Playing"/>,
514         ///     or <see cref="PlayerState.Paused"/> state.</para>
515         ///     <para>If the <paramref name="accurate"/> is true, the play position will be adjusted as the specified <paramref name="position"/> value,
516         ///     but this might be considerably slow. If false, the play position will be a nearest keyframe position.</para>
517         ///     </remarks>
518         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
519         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
520         /// <exception cref="ArgumentOutOfRangeException">The specified position is not valid.</exception>
521         /// <seealso cref="GetPlayPosition"/>
522         public async Task SetPlayPositionAsync(int position, bool accurate)
523         {
524             ValidatePlayerState(PlayerState.Ready, PlayerState.Playing, PlayerState.Paused);
525
526             var taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
527
528             bool immediateResult = _source is MediaStreamSource;
529
530             NativePlayer.SeekCompletedCallback cb = _ => taskCompletionSource.TrySetResult(true);
531
532             using (var cbKeeper = ObjectKeeper.Get(cb))
533             {
534                 SetPlayPosition(position, accurate, cb);
535                 if (immediateResult)
536                 {
537                     taskCompletionSource.TrySetResult(true);
538                 }
539
540                 await taskCompletionSource.Task;
541             }
542         }
543
544         /// <summary>
545         /// Sets the playback rate.
546         /// </summary>
547         /// <param name="rate">The value for the playback rate. Valid range is -5.0 to 5.0, inclusive.</param>
548         /// <remarks>
549         ///     <para>The player must be in the <see cref="PlayerState.Ready"/>, <see cref="PlayerState.Playing"/>,
550         ///     or <see cref="PlayerState.Paused"/> state.</para>
551         ///     <para>The sound will be muted, when the playback rate is under 0.0 or over 2.0.</para>
552         /// </remarks>
553         /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
554         /// <exception cref="InvalidOperationException">
555         ///     The player is not in the valid state.<br/>
556         ///     -or-<br/>
557         ///     Streaming playback.
558         /// </exception>
559         /// <exception cref="ArgumentOutOfRangeException">
560         ///     <paramref name="rate"/> is less than 5.0.<br/>
561         ///     -or-<br/>
562         ///     <paramref name="rate"/> is greater than 5.0.<br/>
563         ///     -or-<br/>
564         ///     <paramref name="rate"/> is zero.
565         /// </exception>
566         public void SetPlaybackRate(float rate)
567         {
568             if (rate < -5.0F || 5.0F < rate || rate == 0.0F)
569             {
570                 throw new ArgumentOutOfRangeException(nameof(rate), rate, "Valid range is -5.0 to 5.0 (except 0.0)");
571             }
572
573             ValidatePlayerState(PlayerState.Ready, PlayerState.Playing, PlayerState.Paused);
574
575             NativePlayer.SetPlaybackRate(Handle, rate).ThrowIfFailed("Failed to set the playback rate.");
576         }
577
578         /// <summary>
579         /// Applies the audio stream policy.
580         /// </summary>
581         /// <param name="policy">The <see cref="AudioStreamPolicy"/> to apply.</param>
582         /// <remarks>
583         /// The player must be in the <see cref="PlayerState.Idle"/> state.<br/>
584         /// <br/>
585         /// <see cref="Player"/> does not support all <see cref="AudioStreamType"/>.<br/>
586         /// Supported types are <see cref="AudioStreamType.Media"/>, <see cref="AudioStreamType.System"/>,
587         /// <see cref="AudioStreamType.Alarm"/>, <see cref="AudioStreamType.Notification"/>,
588         /// <see cref="AudioStreamType.Emergency"/>, <see cref="AudioStreamType.VoiceInformation"/>,
589         /// <see cref="AudioStreamType.RingtoneVoip"/> and <see cref="AudioStreamType.MediaExternalOnly"/>.
590         /// </remarks>
591         /// <exception cref="ObjectDisposedException">
592         ///     The player has already been disposed of.<br/>
593         ///     -or-<br/>
594         ///     <paramref name="policy"/> has already been disposed of.
595         /// </exception>
596         /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
597         /// <exception cref="ArgumentNullException"><paramref name="policy"/> is null.</exception>
598         /// <exception cref="NotSupportedException">
599         ///     <see cref="AudioStreamType"/> of <paramref name="policy"/> is not supported by <see cref="Player"/>.
600         /// </exception>
601         /// <seealso cref="AudioStreamPolicy"/>
602         public void ApplyAudioStreamPolicy(AudioStreamPolicy policy)
603         {
604             if (policy == null)
605             {
606                 throw new ArgumentNullException(nameof(policy));
607             }
608
609             ValidatePlayerState(PlayerState.Idle);
610
611             NativePlayer.SetAudioPolicyInfo(Handle, policy.Handle).
612                 ThrowIfFailed("Failed to set the audio stream policy to the player");
613         }
614         #endregion
615
616         #region Preparing state
617
618         private int _isPreparing;
619
620         private bool IsPreparing()
621         {
622             return Interlocked.CompareExchange(ref _isPreparing, 1, 1) == 1;
623         }
624
625         private void SetPreparing()
626         {
627             Interlocked.Exchange(ref _isPreparing, 1);
628         }
629
630         private void ClearPreparing()
631         {
632             Interlocked.Exchange(ref _isPreparing, 0);
633         }
634
635         #endregion
636
637         /// <summary>
638         /// This method supports the product infrastructure and is not intended to be used directly from application code.
639         /// </summary>
640         protected static Exception GetException(int errorCode, string message) =>
641             ((PlayerErrorCode)errorCode).GetException(message);
642     }
643 }