2 * Copyright (c) 2016 Samsung Electronics Co., Ltd All Rights Reserved
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 using System.Threading.Tasks;
18 using System.Runtime.InteropServices;
19 using System.Diagnostics;
21 using System.Threading;
23 using System.ComponentModel;
25 namespace Tizen.Multimedia
27 internal static class PlayerLog
29 internal const string Tag = "Tizen.Multimedia.Player";
33 /// Provides the ability to control media playback.
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.
40 public partial class Player : IDisposable, IDisplayable<PlayerErrorCode>
42 private PlayerHandle _handle;
45 /// Initializes a new instance of the <see cref="Player"/> class.
49 NativePlayer.Create(out _handle).ThrowIfFailed("Failed to create player");
51 Debug.Assert(_handle != null);
55 if (Features.IsSupported(Features.AudioEffect))
57 _audioEffect = new AudioEffect(this);
60 if (Features.IsSupported(Features.RawVideo))
62 RegisterVideoFrameDecodedCallback();
65 DisplaySettings = PlayerDisplaySettings.Create(this);
68 internal void ValidatePlayerState(params PlayerState[] desiredStates)
70 Debug.Assert(desiredStates.Length > 0);
72 ValidateNotDisposed();
75 if (curState.IsAnyOf(desiredStates))
80 throw new InvalidOperationException($"The player is not in a valid state. " +
81 $"Current State : { curState }, Valid State : { string.Join(", ", desiredStates) }.");
84 #region Dispose support
85 private bool _disposed;
88 /// Releases all resources used by the current instance.
95 private void Dispose(bool disposing)
105 _source.DetachFrom(this);
109 Log.Error(PlayerLog.Tag, e.ToString());
122 internal void ValidateNotDisposed()
126 Log.Warn(PlayerLog.Tag, "player was disposed");
127 throw new ObjectDisposedException(nameof(Player));
131 internal bool IsDisposed => _disposed;
137 /// Gets the streaming download progress.
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/>
144 /// The player is not in the valid state.
146 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
147 public DownloadProgress GetDownloadProgress()
149 ValidatePlayerState(PlayerState.Playing, PlayerState.Paused);
153 NativePlayer.GetStreamingDownloadProgress(Handle, out start, out current).
154 ThrowIfFailed("Failed to get download progress");
156 Log.Info(PlayerLog.Tag, "get download progress : " + start + ", " + current);
158 return new DownloadProgress(start, current);
162 /// Sets the subtitle path for playback.
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>
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)
174 ValidateNotDisposed();
178 throw new ArgumentNullException(nameof(path));
181 if (path.Length == 0)
183 throw new ArgumentException("The path is empty.", nameof(path));
186 if (!File.Exists(path))
188 throw new FileNotFoundException($"The specified file does not exist.", path);
191 NativePlayer.SetSubtitlePath(Handle, path).
192 ThrowIfFailed("Failed to set the subtitle path to the player");
196 /// Removes the subtitle path.
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()
203 ValidatePlayerState(PlayerState.Idle);
205 NativePlayer.SetSubtitlePath(Handle, null).
206 ThrowIfFailed("Failed to clear the subtitle of the player");
210 /// Sets the offset for the subtitle.
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/>
218 /// No subtitle is set.
220 /// <seealso cref="SetSubtitle(string)"/>
221 public void SetSubtitleOffset(int offset)
223 ValidatePlayerState(PlayerState.Playing, PlayerState.Paused);
225 var err = NativePlayer.SetSubtitlePositionOffset(Handle, offset);
227 if (err == PlayerErrorCode.FeatureNotSupported)
229 throw new InvalidOperationException("No subtitle set");
232 err.ThrowIfFailed("Failed to the subtitle offset of the player");
235 private void Prepare()
237 NativePlayer.Prepare(Handle).ThrowIfFailed("Failed to prepare the player");
241 /// Called when the <see cref="Prepare"/> is invoked.
243 protected virtual void OnPreparing()
249 /// Prepares the media player for playback, asynchronously.
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()
261 throw new InvalidOperationException("No source is set.");
264 ValidatePlayerState(PlayerState.Idle);
268 var completionSource = new TaskCompletionSource<bool>();
278 completionSource.SetResult(true);
283 completionSource.TrySetException(e);
287 return completionSource.Task;
291 /// Unprepares the player.
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.
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.
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()
305 if (State == PlayerState.Idle)
307 Log.Warn(PlayerLog.Tag, "idle state already");
310 ValidatePlayerState(PlayerState.Ready, PlayerState.Paused, PlayerState.Playing);
312 NativePlayer.Unprepare(Handle).ThrowIfFailed("Failed to unprepare the player");
318 /// Called after the <see cref="Player"/> is unprepared.
320 /// <seealso cref="Unprepare"/>
321 protected virtual void OnUnprepared()
323 _source?.DetachFrom(this);
328 /// Starts or resumes playback.
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/>
334 /// Sound can be mixed with other sounds if you don't control the stream focus using <see cref="ApplyAudioStreamPolicy"/>.
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()
345 if (State == PlayerState.Playing)
347 Log.Warn(PlayerLog.Tag, "playing state already");
350 ValidatePlayerState(PlayerState.Ready, PlayerState.Paused);
352 NativePlayer.Start(Handle).ThrowIfFailed("Failed to start the player");
356 /// Stops playing the media content.
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.
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()
368 if (State == PlayerState.Ready)
370 Log.Warn(PlayerLog.Tag, "ready state already");
373 ValidatePlayerState(PlayerState.Paused, PlayerState.Playing);
375 NativePlayer.Stop(Handle).ThrowIfFailed("Failed to stop the player");
379 /// Pauses the player.
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.
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()
390 if (State == PlayerState.Paused)
392 Log.Warn(PlayerLog.Tag, "pause state already");
396 ValidatePlayerState(PlayerState.Playing);
398 NativePlayer.Pause(Handle).ThrowIfFailed("Failed to pause the player");
401 private MediaSource _source;
404 /// Sets a media source for the player.
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/>
412 /// It is not able to assign the source to the player.
414 /// <seealso cref="PrepareAsync"/>
415 public void SetSource(MediaSource source)
417 ValidatePlayerState(PlayerState.Idle);
421 source.AttachTo(this);
426 _source.DetachFrom(this);
433 /// Captures a video frame, asynchronously.
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()
443 ValidationUtil.ValidateFeatureSupported(Features.RawVideo);
445 ValidatePlayerState(PlayerState.Playing, PlayerState.Paused);
447 TaskCompletionSource<CapturedFrame> t = new TaskCompletionSource<CapturedFrame>();
449 NativePlayer.VideoCaptureCallback cb = (data, width, height, size, _) =>
451 Debug.Assert(size <= int.MaxValue);
453 byte[] buf = new byte[size];
454 Marshal.Copy(data, buf, 0, (int)size);
456 t.TrySetResult(new CapturedFrame(buf, width, height));
459 using (var cbKeeper = ObjectKeeper.Get(cb))
461 NativePlayer.CaptureVideo(Handle, cb)
462 .ThrowIfFailed("Failed to capture the video");
469 /// Gets the play position in milliseconds.
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()
478 ValidatePlayerState(PlayerState.Ready, PlayerState.Paused, PlayerState.Playing);
480 int playPosition = 0;
482 NativePlayer.GetPlayPosition(Handle, out playPosition).
483 ThrowIfFailed("Failed to get the play position of the player");
485 Log.Info(PlayerLog.Tag, "get play position : " + playPosition);
490 private void SetPlayPosition(int milliseconds, bool accurate,
491 NativePlayer.SeekCompletedCallback cb)
493 var ret = NativePlayer.SetPlayPosition(Handle, milliseconds, accurate, cb, IntPtr.Zero);
495 //Note that we assume invalid param error is returned only when the position value is invalid.
496 if (ret == PlayerErrorCode.InvalidArgument)
498 throw new ArgumentOutOfRangeException(nameof(milliseconds), milliseconds,
499 "The position is not valid.");
501 if (ret != PlayerErrorCode.None)
503 Log.Error(PlayerLog.Tag, "Failed to set play position, " + (PlayerError)ret);
505 ret.ThrowIfFailed("Failed to set play position");
509 /// Sets the seek position for playback, asynchronously.
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>
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>
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)
525 ValidatePlayerState(PlayerState.Ready, PlayerState.Playing, PlayerState.Paused);
527 var taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
529 bool immediateResult = _source is MediaStreamSource;
531 NativePlayer.SeekCompletedCallback cb = _ => taskCompletionSource.TrySetResult(true);
533 using (var cbKeeper = ObjectKeeper.Get(cb))
535 SetPlayPosition(position, accurate, cb);
538 taskCompletionSource.TrySetResult(true);
541 await taskCompletionSource.Task;
546 /// Sets the playback rate.
548 /// <param name="rate">The value for the playback rate. Valid range is -5.0 to 5.0, inclusive.</param>
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>
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/>
558 /// Streaming playback.
560 /// <exception cref="ArgumentOutOfRangeException">
561 /// <paramref name="rate"/> is less than 5.0.<br/>
563 /// <paramref name="rate"/> is greater than 5.0.<br/>
565 /// <paramref name="rate"/> is zero.
567 public void SetPlaybackRate(float rate)
569 if (rate < -5.0F || 5.0F < rate || rate == 0.0F)
571 throw new ArgumentOutOfRangeException(nameof(rate), rate, "Valid range is -5.0 to 5.0 (except 0.0)");
574 ValidatePlayerState(PlayerState.Ready, PlayerState.Playing, PlayerState.Paused);
576 NativePlayer.SetPlaybackRate(Handle, rate).ThrowIfFailed("Failed to set the playback rate.");
580 /// Applies the audio stream policy.
582 /// <param name="policy">The <see cref="AudioStreamPolicy"/> to apply.</param>
584 /// The player must be in the <see cref="PlayerState.Idle"/> state.<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"/>.
592 /// <exception cref="ObjectDisposedException">
593 /// The player has already been disposed of.<br/>
595 /// <paramref name="policy"/> has already been disposed of.
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"/>.
602 /// <seealso cref="AudioStreamPolicy"/>
603 public void ApplyAudioStreamPolicy(AudioStreamPolicy policy)
607 throw new ArgumentNullException(nameof(policy));
610 ValidatePlayerState(PlayerState.Idle);
612 NativePlayer.SetAudioPolicyInfo(Handle, policy.Handle).
613 ThrowIfFailed("Failed to set the audio stream policy to the player");
617 #region Preparing state
619 private int _isPreparing;
621 private bool IsPreparing()
623 return Interlocked.CompareExchange(ref _isPreparing, 1, 1) == 1;
626 private void SetPreparing()
628 Interlocked.Exchange(ref _isPreparing, 1);
631 private void ClearPreparing()
633 Interlocked.Exchange(ref _isPreparing, 0);
639 /// This method supports the product infrastructure and is not intended to be used directly from application code.
641 [EditorBrowsable(EditorBrowsableState.Never)]
642 protected static Exception GetException(int errorCode, string message) =>
643 ((PlayerErrorCode)errorCode).GetException(message);