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