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.
47 /// <since_tizen> 3 </since_tizen>
50 NativePlayer.Create(out _handle).ThrowIfFailed(null, "Failed to create player");
52 Debug.Assert(_handle != null);
54 if (Features.IsSupported(PlayerFeatures.AudioEffect))
56 _audioEffect = new AudioEffect(this);
59 if (Features.IsSupported(PlayerFeatures.RawVideo))
61 RegisterVideoFrameDecodedCallback();
64 DisplaySettings = PlayerDisplaySettings.Create(this);
67 internal void ValidatePlayerState(params PlayerState[] desiredStates)
69 Debug.Assert(desiredStates.Length > 0);
71 ValidateNotDisposed();
74 if (curState.IsAnyOf(desiredStates))
79 throw new InvalidOperationException($"The player is not in a valid state. " +
80 $"Current State : { curState }, Valid State : { string.Join(", ", desiredStates) }.");
83 #region Dispose support
84 private bool _disposed;
87 /// Releases all resources used by the current instance.
89 /// <since_tizen> 3 </since_tizen>
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 /// <since_tizen> 3 </since_tizen>
148 public DownloadProgress GetDownloadProgress()
150 ValidatePlayerState(PlayerState.Playing, PlayerState.Paused);
154 NativePlayer.GetStreamingDownloadProgress(Handle, out start, out current).
155 ThrowIfFailed(this, "Failed to get download progress");
157 Log.Info(PlayerLog.Tag, "get download progress : " + start + ", " + current);
159 return new DownloadProgress(start, current);
163 /// Sets the subtitle path for playback.
165 /// <remarks>Only MicroDVD/SubViewer(*.sub), SAMI(*.smi), and SubRip(*.srt) subtitle formats are supported.
166 /// <para>The mediastorage privilege(http://tizen.org/privilege/mediastorage) must be added if any files are used to play located in the internal storage.
167 /// The externalstorage privilege(http://tizen.org/privilege/externalstorage) must be added if any files are used to play located in the external storage.</para>
169 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
170 /// <exception cref="ArgumentException"><paramref name="path"/> is an empty string.</exception>
171 /// <exception cref="FileNotFoundException">The specified path does not exist.</exception>
172 /// <exception cref="ArgumentNullException"><paramref name="path"/> is null.</exception>
173 /// <since_tizen> 3 </since_tizen>
174 public void SetSubtitle(string path)
176 ValidateNotDisposed();
180 throw new ArgumentNullException(nameof(path));
183 if (path.Length == 0)
185 throw new ArgumentException("The path is empty.", nameof(path));
188 if (!File.Exists(path))
190 throw new FileNotFoundException($"The specified file does not exist.", path);
193 NativePlayer.SetSubtitlePath(Handle, path).
194 ThrowIfFailed(this, "Failed to set the subtitle path to the player");
198 /// Removes the subtitle path.
200 /// <remarks>The player must be in the <see cref="PlayerState.Idle"/> state.</remarks>
201 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
202 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
203 /// <since_tizen> 3 </since_tizen>
204 public void ClearSubtitle()
206 ValidatePlayerState(PlayerState.Idle);
208 NativePlayer.SetSubtitlePath(Handle, null).
209 ThrowIfFailed(this, "Failed to clear the subtitle of the player");
213 /// Sets the offset for the subtitle.
215 /// <param name="offset">The value indicating a desired offset in milliseconds.</param>
216 /// <remarks>The player must be in the <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.</remarks>
217 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
218 /// <exception cref="InvalidOperationException">
219 /// The player is not in the valid state.<br/>
221 /// No subtitle is set.
223 /// <seealso cref="SetSubtitle(string)"/>
224 /// <since_tizen> 3 </since_tizen>
225 public void SetSubtitleOffset(int offset)
227 ValidatePlayerState(PlayerState.Playing, PlayerState.Paused);
229 var err = NativePlayer.SetSubtitlePositionOffset(Handle, offset);
231 if (err == PlayerErrorCode.FeatureNotSupported)
233 throw new InvalidOperationException("No subtitle set");
236 err.ThrowIfFailed(this, "Failed to the subtitle offset of the player");
239 private void Prepare()
241 NativePlayer.Prepare(Handle).ThrowIfFailed(this, "Failed to prepare the player");
245 /// Called when the <see cref="Prepare"/> is invoked.
247 /// <since_tizen> 3 </since_tizen>
248 protected virtual void OnPreparing()
254 /// Prepares the media player for playback, asynchronously.
256 /// <returns>A task that represents the asynchronous prepare operation.</returns>
257 /// <remarks>To prepare the player, the player must be in the <see cref="PlayerState.Idle"/> state,
258 /// and a source must be set.</remarks>
259 /// <exception cref="InvalidOperationException">No source is set.</exception>
260 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
261 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
262 /// <since_tizen> 3 </since_tizen>
263 public virtual Task PrepareAsync()
267 throw new InvalidOperationException("No source is set.");
270 ValidatePlayerState(PlayerState.Idle);
272 SetDisplay(_display).ThrowIfFailed(this, "Failed to configure display of the player");
278 return Task.Factory.StartNew(() =>
288 }, CancellationToken.None,
289 TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning,
290 TaskScheduler.Default);
294 /// Unprepares the player.
297 /// The most recently used source is reset and is no longer associated with the player. Playback is no longer possible.
298 /// If you want to use the player again, you have to set a source and call <see cref="PrepareAsync"/> again.
300 /// The player must be in the <see cref="PlayerState.Ready"/>, <see cref="PlayerState.Playing"/>, or <see cref="PlayerState.Paused"/> state.
301 /// It has no effect if the player is already in the <see cref="PlayerState.Idle"/> state.
304 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
305 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
306 /// <since_tizen> 3 </since_tizen>
307 public virtual void Unprepare()
309 if (State == PlayerState.Idle)
311 Log.Warn(PlayerLog.Tag, "idle state already");
314 ValidatePlayerState(PlayerState.Ready, PlayerState.Paused, PlayerState.Playing);
316 NativePlayer.Unprepare(Handle).ThrowIfFailed(this, "Failed to unprepare the player");
322 /// Called after the <see cref="Player"/> is unprepared.
324 /// <seealso cref="Unprepare"/>
325 /// <since_tizen> 3 </since_tizen>
326 protected virtual void OnUnprepared()
328 _source?.DetachFrom(this);
333 /// Starts or resumes playback.
336 /// The player must be in the <see cref="PlayerState.Ready"/> or <see cref="PlayerState.Paused"/> state.
337 /// It has no effect if the player is already in the <see cref="PlayerState.Playing"/> state.<br/>
339 /// Sound can be mixed with other sounds if you don't control the stream focus using <see cref="ApplyAudioStreamPolicy"/>.
341 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
342 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
343 /// <seealso cref="PrepareAsync"/>
344 /// <seealso cref="Stop"/>
345 /// <seealso cref="Pause"/>
346 /// <seealso cref="PlaybackCompleted"/>
347 /// <seealso cref="ApplyAudioStreamPolicy"/>
348 /// <since_tizen> 3 </since_tizen>
349 public virtual void Start()
351 if (State == PlayerState.Playing)
353 Log.Warn(PlayerLog.Tag, "playing state already");
356 ValidatePlayerState(PlayerState.Ready, PlayerState.Paused);
358 NativePlayer.Start(Handle).ThrowIfFailed(this, "Failed to start the player");
362 /// Stops playing the media content.
365 /// The player must be in the <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.
366 /// It has no effect if the player is already in the <see cref="PlayerState.Ready"/> state.
368 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
369 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
370 /// <seealso cref="Start"/>
371 /// <seealso cref="Pause"/>
372 /// <since_tizen> 3 </since_tizen>
373 public virtual void Stop()
375 if (State == PlayerState.Ready)
377 Log.Warn(PlayerLog.Tag, "ready state already");
380 ValidatePlayerState(PlayerState.Paused, PlayerState.Playing);
382 NativePlayer.Stop(Handle).ThrowIfFailed(this, "Failed to stop the player");
386 /// Pauses the player.
389 /// The player must be in the <see cref="PlayerState.Playing"/> state.
390 /// It has no effect if the player is already in the <see cref="PlayerState.Paused"/> state.
392 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
393 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
394 /// <seealso cref="Start"/>
395 /// <since_tizen> 3 </since_tizen>
396 public virtual void Pause()
398 if (State == PlayerState.Paused)
400 Log.Warn(PlayerLog.Tag, "pause state already");
404 ValidatePlayerState(PlayerState.Playing);
406 NativePlayer.Pause(Handle).ThrowIfFailed(this, "Failed to pause the player");
409 private MediaSource _source;
412 /// Sets a media source for the player.
414 /// <param name="source">A <see cref="MediaSource"/> that specifies the source for playback.</param>
415 /// <remarks>The player must be in the <see cref="PlayerState.Idle"/> state.</remarks>
416 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
417 /// <exception cref="InvalidOperationException">
418 /// The player is not in the valid state.<br/>
420 /// It is not able to assign the source to the player.
422 /// <seealso cref="PrepareAsync"/>
423 /// <since_tizen> 3 </since_tizen>
424 public void SetSource(MediaSource source)
426 ValidatePlayerState(PlayerState.Idle);
430 source.AttachTo(this);
435 _source.DetachFrom(this);
442 /// Captures a video frame, asynchronously.
444 /// <returns>A task that represents the asynchronous capture operation.</returns>
445 /// <feature>http://tizen.org/feature/multimedia.raw_video</feature>
446 /// <remarks>The player must be in the <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.</remarks>
447 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
448 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
449 /// <exception cref="NotSupportedException">The required feature is not supported.</exception>
450 /// <since_tizen> 3 </since_tizen>
451 public async Task<CapturedFrame> CaptureVideoAsync()
453 ValidationUtil.ValidateFeatureSupported(PlayerFeatures.RawVideo);
455 ValidatePlayerState(PlayerState.Playing, PlayerState.Paused);
457 TaskCompletionSource<CapturedFrame> t = new TaskCompletionSource<CapturedFrame>();
459 NativePlayer.VideoCaptureCallback cb = (data, width, height, size, _) =>
461 Debug.Assert(size <= int.MaxValue);
463 byte[] buf = new byte[size];
464 Marshal.Copy(data, buf, 0, (int)size);
466 t.TrySetResult(new CapturedFrame(buf, width, height));
469 using (var cbKeeper = ObjectKeeper.Get(cb))
471 NativePlayer.CaptureVideo(Handle, cb)
472 .ThrowIfFailed(this, "Failed to capture the video");
479 /// Gets the play position in milliseconds.
481 /// <remarks>The player must be in the <see cref="PlayerState.Ready"/>, <see cref="PlayerState.Playing"/>,
482 /// or <see cref="PlayerState.Paused"/> state.</remarks>
483 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
484 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
485 /// <seealso cref="SetPlayPositionAsync(int, bool)"/>
486 /// <since_tizen> 3 </since_tizen>
487 public int GetPlayPosition()
489 ValidatePlayerState(PlayerState.Ready, PlayerState.Paused, PlayerState.Playing);
491 int playPosition = 0;
493 NativePlayer.GetPlayPosition(Handle, out playPosition).
494 ThrowIfFailed(this, "Failed to get the play position of the player");
496 Log.Info(PlayerLog.Tag, "get play position : " + playPosition);
501 private void SetPlayPosition(int milliseconds, bool accurate,
502 NativePlayer.SeekCompletedCallback cb)
504 var ret = NativePlayer.SetPlayPosition(Handle, milliseconds, accurate, cb, IntPtr.Zero);
506 //Note that we assume invalid param error is returned only when the position value is invalid.
507 if (ret == PlayerErrorCode.InvalidArgument)
509 throw new ArgumentOutOfRangeException(nameof(milliseconds), milliseconds,
510 "The position is not valid.");
512 if (ret != PlayerErrorCode.None)
514 Log.Error(PlayerLog.Tag, "Failed to set play position, " + (PlayerError)ret);
516 ret.ThrowIfFailed(this, "Failed to set play position");
520 /// Sets the seek position for playback, asynchronously.
522 /// <param name="position">The value indicating a desired position in milliseconds.</param>
523 /// <param name="accurate">The value indicating whether the operation performs with accuracy.</param>
525 /// <para>The player must be in the <see cref="PlayerState.Ready"/>, <see cref="PlayerState.Playing"/>,
526 /// or <see cref="PlayerState.Paused"/> state.</para>
527 /// <para>If the <paramref name="accurate"/> is true, the play position will be adjusted as the specified <paramref name="position"/> value,
528 /// but this might be considerably slow. If false, the play position will be a nearest keyframe position.</para>
530 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
531 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
532 /// <exception cref="ArgumentOutOfRangeException">The specified position is not valid.</exception>
533 /// <seealso cref="GetPlayPosition"/>
534 /// <since_tizen> 3 </since_tizen>
535 public async Task SetPlayPositionAsync(int position, bool accurate)
537 ValidatePlayerState(PlayerState.Ready, PlayerState.Playing, PlayerState.Paused);
539 var taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
541 bool immediateResult = _source is MediaStreamSource;
543 NativePlayer.SeekCompletedCallback cb = _ => taskCompletionSource.TrySetResult(true);
545 using (var cbKeeper = ObjectKeeper.Get(cb))
547 SetPlayPosition(position, accurate, cb);
550 taskCompletionSource.TrySetResult(true);
553 await taskCompletionSource.Task;
558 /// Sets the playback rate.
560 /// <param name="rate">The value for the playback rate. Valid range is -5.0 to 5.0, inclusive.</param>
562 /// <para>The player must be in the <see cref="PlayerState.Ready"/>, <see cref="PlayerState.Playing"/>,
563 /// or <see cref="PlayerState.Paused"/> state.</para>
564 /// <para>The sound will be muted, when the playback rate is under 0.0 or over 2.0.</para>
566 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
567 /// <exception cref="InvalidOperationException">
568 /// The player is not in the valid state.<br/>
570 /// Streaming playback.
572 /// <exception cref="ArgumentOutOfRangeException">
573 /// <paramref name="rate"/> is less than 5.0.<br/>
575 /// <paramref name="rate"/> is greater than 5.0.<br/>
577 /// <paramref name="rate"/> is zero.
579 /// <since_tizen> 3 </since_tizen>
580 public void SetPlaybackRate(float rate)
582 if (rate < -5.0F || 5.0F < rate || rate == 0.0F)
584 throw new ArgumentOutOfRangeException(nameof(rate), rate, "Valid range is -5.0 to 5.0 (except 0.0)");
587 ValidatePlayerState(PlayerState.Ready, PlayerState.Playing, PlayerState.Paused);
589 NativePlayer.SetPlaybackRate(Handle, rate).ThrowIfFailed(this, "Failed to set the playback rate.");
593 /// Applies the audio stream policy.
595 /// <param name="policy">The <see cref="AudioStreamPolicy"/> to apply.</param>
597 /// The player must be in the <see cref="PlayerState.Idle"/> state.<br/>
599 /// <see cref="Player"/> does not support all <see cref="AudioStreamType"/>.<br/>
600 /// Supported types are <see cref="AudioStreamType.Media"/>, <see cref="AudioStreamType.System"/>,
601 /// <see cref="AudioStreamType.Alarm"/>, <see cref="AudioStreamType.Notification"/>,
602 /// <see cref="AudioStreamType.Emergency"/>, <see cref="AudioStreamType.VoiceInformation"/>,
603 /// <see cref="AudioStreamType.RingtoneVoip"/> and <see cref="AudioStreamType.MediaExternalOnly"/>.
605 /// <exception cref="ObjectDisposedException">
606 /// The player has already been disposed of.<br/>
608 /// <paramref name="policy"/> has already been disposed of.
610 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
611 /// <exception cref="ArgumentNullException"><paramref name="policy"/> is null.</exception>
612 /// <exception cref="NotSupportedException">
613 /// The required feature is not supported.<br/>
615 /// <see cref="AudioStreamType"/> of <paramref name="policy"/> is not supported on the current platform.
617 /// <seealso cref="AudioStreamPolicy"/>
618 /// <feature>http://tizen.org/feature/multimedia.player.stream_info</feature>
619 /// <since_tizen> 3 </since_tizen>
620 public void ApplyAudioStreamPolicy(AudioStreamPolicy policy)
622 ValidationUtil.ValidateFeatureSupported("http://tizen.org/feature/multimedia.player.stream_info");
626 throw new ArgumentNullException(nameof(policy));
629 ValidatePlayerState(PlayerState.Idle);
631 var ret = NativePlayer.SetAudioPolicyInfo(Handle, policy.Handle);
633 if (ret == PlayerErrorCode.InvalidArgument)
635 throw new NotSupportedException("The specified policy is not supported on the current system.");
638 ret.ThrowIfFailed(this, "Failed to set the audio stream policy to the player");
642 #region Preparing state
644 private int _isPreparing;
646 private bool IsPreparing()
648 return Interlocked.CompareExchange(ref _isPreparing, 1, 1) == 1;
651 private void SetPreparing()
653 Interlocked.Exchange(ref _isPreparing, 1);
656 private void ClearPreparing()
658 Interlocked.Exchange(ref _isPreparing, 0);