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;
24 namespace Tizen.Multimedia
26 internal static class PlayerLog
28 internal const string Tag = "Tizen.Multimedia.Player";
32 /// Provides the ability to control media playback.
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.
39 public partial class Player : IDisposable, IDisplayable<PlayerErrorCode>
41 private PlayerHandle _handle;
44 /// Initializes a new instance of the <see cref="Player"/> class.
48 NativePlayer.Create(out _handle).ThrowIfFailed("Failed to create player");
50 Debug.Assert(_handle != null);
54 if (Features.IsSupported(Features.AudioEffect))
56 _audioEffect = new AudioEffect(this);
59 if (Features.IsSupported(Features.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.
94 private void Dispose(bool disposing)
104 _source.DetachFrom(this);
108 Log.Error(PlayerLog.Tag, e.ToString());
121 internal void ValidateNotDisposed()
125 Log.Warn(PlayerLog.Tag, "player was disposed");
126 throw new ObjectDisposedException(nameof(Player));
130 internal bool IsDisposed => _disposed;
136 /// Gets the streaming download progress.
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/>
143 /// The player is not in the valid state.
145 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
146 public DownloadProgress GetDownloadProgress()
148 ValidatePlayerState(PlayerState.Playing, PlayerState.Paused);
152 NativePlayer.GetStreamingDownloadProgress(Handle, out start, out current).
153 ThrowIfFailed("Failed to get download progress");
155 Log.Info(PlayerLog.Tag, "get download progress : " + start + ", " + current);
157 return new DownloadProgress(start, current);
161 /// Sets the subtitle path for playback.
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>
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)
173 ValidateNotDisposed();
177 throw new ArgumentNullException(nameof(path));
180 if (path.Length == 0)
182 throw new ArgumentException("The path is empty.", nameof(path));
185 if (!File.Exists(path))
187 throw new FileNotFoundException($"The specified file does not exist.", path);
190 NativePlayer.SetSubtitlePath(Handle, path).
191 ThrowIfFailed("Failed to set the subtitle path to the player");
195 /// Removes the subtitle path.
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()
202 ValidatePlayerState(PlayerState.Idle);
204 NativePlayer.SetSubtitlePath(Handle, null).
205 ThrowIfFailed("Failed to clear the subtitle of the player");
209 /// Sets the offset for the subtitle.
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/>
217 /// No subtitle is set.
219 /// <seealso cref="SetSubtitle(string)"/>
220 public void SetSubtitleOffset(int offset)
222 ValidatePlayerState(PlayerState.Playing, PlayerState.Paused);
224 var err = NativePlayer.SetSubtitlePositionOffset(Handle, offset);
226 if (err == PlayerErrorCode.FeatureNotSupported)
228 throw new InvalidOperationException("No subtitle set");
231 err.ThrowIfFailed("Failed to the subtitle offset of the player");
234 private void Prepare()
236 NativePlayer.Prepare(Handle).ThrowIfFailed("Failed to prepare the player");
240 /// Called when the <see cref="Prepare"/> is invoked.
242 protected virtual void OnPreparing()
248 /// Prepares the media player for playback, asynchronously.
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()
260 throw new InvalidOperationException("No source is set.");
263 ValidatePlayerState(PlayerState.Idle);
267 var completionSource = new TaskCompletionSource<bool>();
277 completionSource.SetResult(true);
282 completionSource.TrySetException(e);
286 return completionSource.Task;
290 /// Unprepares the player.
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.
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.
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()
304 if (State == PlayerState.Idle)
306 Log.Warn(PlayerLog.Tag, "idle state already");
309 ValidatePlayerState(PlayerState.Ready, PlayerState.Paused, PlayerState.Playing);
311 NativePlayer.Unprepare(Handle).ThrowIfFailed("Failed to unprepare the player");
317 /// Called after the <see cref="Player"/> is unprepared.
319 /// <seealso cref="Unprepare"/>
320 protected virtual void OnUnprepared()
322 _source?.DetachFrom(this);
327 /// Starts or resumes playback.
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/>
333 /// Sound can be mixed with other sounds if you don't control the stream focus using <see cref="ApplyAudioStreamPolicy"/>.
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()
344 if (State == PlayerState.Playing)
346 Log.Warn(PlayerLog.Tag, "playing state already");
349 ValidatePlayerState(PlayerState.Ready, PlayerState.Paused);
351 NativePlayer.Start(Handle).ThrowIfFailed("Failed to start the player");
355 /// Stops playing the media content.
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.
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()
367 if (State == PlayerState.Ready)
369 Log.Warn(PlayerLog.Tag, "ready state already");
372 ValidatePlayerState(PlayerState.Paused, PlayerState.Playing);
374 NativePlayer.Stop(Handle).ThrowIfFailed("Failed to stop the player");
378 /// Pauses the player.
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.
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()
389 if (State == PlayerState.Paused)
391 Log.Warn(PlayerLog.Tag, "pause state already");
395 ValidatePlayerState(PlayerState.Playing);
397 NativePlayer.Pause(Handle).ThrowIfFailed("Failed to pause the player");
400 private MediaSource _source;
403 /// Sets a media source for the player.
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/>
411 /// It is not able to assign the source to the player.
413 /// <seealso cref="PrepareAsync"/>
414 public void SetSource(MediaSource source)
416 ValidatePlayerState(PlayerState.Idle);
420 source.AttachTo(this);
425 _source.DetachFrom(this);
432 /// Captures a video frame, asynchronously.
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()
442 ValidationUtil.ValidateFeatureSupported(Features.RawVideo);
444 ValidatePlayerState(PlayerState.Playing, PlayerState.Paused);
446 TaskCompletionSource<CapturedFrame> t = new TaskCompletionSource<CapturedFrame>();
448 NativePlayer.VideoCaptureCallback cb = (data, width, height, size, _) =>
450 Debug.Assert(size <= int.MaxValue);
452 byte[] buf = new byte[size];
453 Marshal.Copy(data, buf, 0, (int)size);
455 t.TrySetResult(new CapturedFrame(buf, width, height));
458 using (var cbKeeper = ObjectKeeper.Get(cb))
460 NativePlayer.CaptureVideo(Handle, cb)
461 .ThrowIfFailed("Failed to capture the video");
468 /// Gets the play position in milliseconds.
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()
477 ValidatePlayerState(PlayerState.Ready, PlayerState.Paused, PlayerState.Playing);
479 int playPosition = 0;
481 NativePlayer.GetPlayPosition(Handle, out playPosition).
482 ThrowIfFailed("Failed to get the play position of the player");
484 Log.Info(PlayerLog.Tag, "get play position : " + playPosition);
489 private void SetPlayPosition(int milliseconds, bool accurate,
490 NativePlayer.SeekCompletedCallback cb)
492 var ret = NativePlayer.SetPlayPosition(Handle, milliseconds, accurate, cb, IntPtr.Zero);
494 //Note that we assume invalid param error is returned only when the position value is invalid.
495 if (ret == PlayerErrorCode.InvalidArgument)
497 throw new ArgumentOutOfRangeException(nameof(milliseconds), milliseconds,
498 "The position is not valid.");
500 if (ret != PlayerErrorCode.None)
502 Log.Error(PlayerLog.Tag, "Failed to set play position, " + (PlayerError)ret);
504 ret.ThrowIfFailed("Failed to set play position");
508 /// Sets the seek position for playback, asynchronously.
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>
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>
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)
524 ValidatePlayerState(PlayerState.Ready, PlayerState.Playing, PlayerState.Paused);
526 var taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
528 bool immediateResult = _source is MediaStreamSource;
530 NativePlayer.SeekCompletedCallback cb = _ => taskCompletionSource.TrySetResult(true);
532 using (var cbKeeper = ObjectKeeper.Get(cb))
534 SetPlayPosition(position, accurate, cb);
537 taskCompletionSource.TrySetResult(true);
540 await taskCompletionSource.Task;
545 /// Sets the playback rate.
547 /// <param name="rate">The value for the playback rate. Valid range is -5.0 to 5.0, inclusive.</param>
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>
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/>
557 /// Streaming playback.
559 /// <exception cref="ArgumentOutOfRangeException">
560 /// <paramref name="rate"/> is less than 5.0.<br/>
562 /// <paramref name="rate"/> is greater than 5.0.<br/>
564 /// <paramref name="rate"/> is zero.
566 public void SetPlaybackRate(float rate)
568 if (rate < -5.0F || 5.0F < rate || rate == 0.0F)
570 throw new ArgumentOutOfRangeException(nameof(rate), rate, "Valid range is -5.0 to 5.0 (except 0.0)");
573 ValidatePlayerState(PlayerState.Ready, PlayerState.Playing, PlayerState.Paused);
575 NativePlayer.SetPlaybackRate(Handle, rate).ThrowIfFailed("Failed to set the playback rate.");
579 /// Applies the audio stream policy.
581 /// <param name="policy">The <see cref="AudioStreamPolicy"/> to apply.</param>
583 /// The player must be in the <see cref="PlayerState.Idle"/> state.<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"/>.
591 /// <exception cref="ObjectDisposedException">
592 /// The player has already been disposed of.<br/>
594 /// <paramref name="policy"/> has already been disposed of.
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"/>.
601 /// <seealso cref="AudioStreamPolicy"/>
602 public void ApplyAudioStreamPolicy(AudioStreamPolicy policy)
606 throw new ArgumentNullException(nameof(policy));
609 ValidatePlayerState(PlayerState.Idle);
611 NativePlayer.SetAudioPolicyInfo(Handle, policy.Handle).
612 ThrowIfFailed("Failed to set the audio stream policy to the player");
616 #region Preparing state
618 private int _isPreparing;
620 private bool IsPreparing()
622 return Interlocked.CompareExchange(ref _isPreparing, 1, 1) == 1;
625 private void SetPreparing()
627 Interlocked.Exchange(ref _isPreparing, 1);
630 private void ClearPreparing()
632 Interlocked.Exchange(ref _isPreparing, 0);
638 /// This method supports the product infrastructure and is not intended to be used directly from application code.
640 protected static Exception GetException(int errorCode, string message) =>
641 ((PlayerErrorCode)errorCode).GetException(message);