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 static internal class PlayerLog
28 internal const string Tag = "Tizen.Multimedia.Player";
29 internal const string Enter = "[ENTER]";
30 internal const string Leave = "[LEAVE]";
34 /// Provides the ability to control media playback.
37 /// The Player provides functions to play a media content.
38 /// It also provides functions to adjust the configurations of the player such as playback rate, volume, looping etc.
39 /// Note that only one video player can be played at one time.
41 public partial class Player : IDisposable, IDisplayable<PlayerErrorCode>
43 private PlayerHandle _handle;
46 /// Initialize a new instance of the Player class.
50 NativePlayer.Create(out _handle).ThrowIfFailed("Failed to create player");
52 Debug.Assert(_handle != null);
56 if (Features.IsSupported(Features.AudioEffect))
58 _audioEffect = new AudioEffect(this);
61 if (Features.IsSupported(Features.RawVideo))
63 RegisterVideoFrameDecodedCallback();
66 DisplaySettings = PlayerDisplaySettings.Create(this);
69 internal void ValidatePlayerState(params PlayerState[] desiredStates)
71 Debug.Assert(desiredStates.Length > 0);
73 ValidateNotDisposed();
76 if (curState.IsAnyOf(desiredStates))
81 throw new InvalidOperationException($"The player is not in a valid state. " +
82 $"Current State : { curState }, Valid State : { string.Join(", ", desiredStates) }.");
85 #region Dispose support
86 private bool _disposed;
89 /// Releases all resources used by the current instance.
96 private void Dispose(bool disposing)
100 ReplaceDisplay(null);
106 _source.DetachFrom(this);
110 Log.Error(PlayerLog.Tag, e.ToString());
123 internal void ValidateNotDisposed()
127 Log.Warn(PlayerLog.Tag, "player was disposed");
128 throw new ObjectDisposedException(nameof(Player));
132 internal bool IsDisposed => _disposed;
138 /// Gets the streaming download Progress.
140 /// <returns>The <see cref="DownloadProgress"/> containing current download progress.</returns>
141 /// <remarks>The player must be in the <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.</remarks>
142 /// <exception cref="InvalidOperationException">
143 /// The player is not streaming.\n
145 /// The player is not in the valid state.
147 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
148 public DownloadProgress GetDownloadProgress()
150 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
151 ValidatePlayerState(PlayerState.Playing, PlayerState.Paused);
155 NativePlayer.GetStreamingDownloadProgress(Handle, out start, out current).
156 ThrowIfFailed("Failed to get download progress");
158 Log.Info(PlayerLog.Tag, "get download progress : " + start + ", " + current);
160 return new DownloadProgress(start, current);
164 /// Sets the subtitle path for playback.
166 /// <remarks>Only MicroDVD/SubViewer(*.sub), SAMI(*.smi), and SubRip(*.srt) subtitle formats are supported.
167 /// <para>The mediastorage privilege(http://tizen.org/privilege/mediastorage) must be added if any files are used to play located in the internal storage.
168 /// The externalstorage privilege(http://tizen.org/privilege/externalstorage) must be added if any files are used to play located in the external storage.</para>
170 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
171 /// <exception cref="ArgumentException"><paramref name="path"/> is an empty string.</exception>
172 /// <exception cref="FileNotFoundException">The specified path does not exist.</exception>
173 /// <exception cref="ArgumentNullException">The path is null.</exception>
174 public void SetSubtitle(string path)
176 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
177 ValidateNotDisposed();
181 throw new ArgumentNullException(nameof(path));
184 if (path.Length == 0)
186 throw new ArgumentException("The path is empty.", nameof(path));
189 if (!File.Exists(path))
191 throw new FileNotFoundException($"The specified file does not exist.", path);
194 NativePlayer.SetSubtitlePath(Handle, path).
195 ThrowIfFailed("Failed to set the subtitle path to the player");
197 Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
201 /// Removes the subtitle path.
203 /// <remarks>The player must be in the <see cref="PlayerState.Idle"/> state.</remarks>
204 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
205 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
206 public void ClearSubtitle()
208 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
209 ValidatePlayerState(PlayerState.Idle);
211 NativePlayer.SetSubtitlePath(Handle, null).
212 ThrowIfFailed("Failed to clear the subtitle of the player");
213 Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
217 /// Sets the offset for the subtitle.
219 /// <param name="offset">The value indicating a desired offset in milliseconds.</param>
220 /// <remarks>The player must be in the <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.</remarks>
221 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
222 /// <exception cref="InvalidOperationException">
223 /// The player is not in the valid state.\n
225 /// No subtitle is set.
227 /// <seealso cref="SetSubtitle(string)"/>
228 public void SetSubtitleOffset(int offset)
230 ValidatePlayerState(PlayerState.Playing, PlayerState.Paused);
232 var err = NativePlayer.SetSubtitlePositionOffset(Handle, offset);
234 if (err == PlayerErrorCode.FeatureNotSupported)
236 throw new InvalidOperationException("No subtitle set");
239 err.ThrowIfFailed("Failed to the subtitle offset of the player");
240 Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
243 private void Prepare()
245 NativePlayer.Prepare(Handle).ThrowIfFailed("Failed to prepare the player");
249 /// Called when the <see cref="Prepare"/> is invoked.
251 protected virtual void OnPreparing()
257 /// Prepares the media player for playback, asynchronously.
259 /// <returns>A task that represents the asynchronous prepare operation.</returns>
260 /// <remarks>To prepare the player, the player must be in the <see cref="PlayerState.Idle"/> state,
261 /// and a source must be set.</remarks>
262 /// <exception cref="InvalidOperationException">No source is set.</exception>
263 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
264 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
265 public virtual Task PrepareAsync()
267 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
270 throw new InvalidOperationException("No source is set.");
273 ValidatePlayerState(PlayerState.Idle);
277 var completionSource = new TaskCompletionSource<bool>();
287 completionSource.SetResult(true);
292 completionSource.TrySetException(e);
295 Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
297 return completionSource.Task;
301 /// Unprepares the player.
304 /// The most recently used source is reset and no longer associated with the player. Playback is no longer possible.
305 /// If you want to use the player again, you have to set a source and call <see cref="PrepareAsync"/> again.
307 /// The player must be in the <see cref="PlayerState.Ready"/>, <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.
308 /// It has no effect if the player is already in the <see cref="PlayerState.Idle"/> state.
311 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
312 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
313 public virtual void Unprepare()
315 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
316 if (State == PlayerState.Idle)
318 Log.Warn(PlayerLog.Tag, "idle state already");
321 ValidatePlayerState(PlayerState.Ready, PlayerState.Paused, PlayerState.Playing);
323 NativePlayer.Unprepare(Handle).ThrowIfFailed("Failed to unprepare the player");
329 /// Called after the <see cref="Player"/> is unprepared.
331 /// <seealso cref="Unprepare"/>
332 protected virtual void OnUnprepared()
334 _source?.DetachFrom(this);
339 /// Starts or resumes playback.
342 /// The player must be in the <see cref="PlayerState.Ready"/> or <see cref="PlayerState.Paused"/> state.
343 /// It has no effect if the player is already in the <see cref="PlayerState.Playing"/> state.\n
345 /// Sound can be mixed with other sounds if you don't control the stream focus using <see cref="ApplyAudioStreamPolicy"/>.
347 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
348 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
349 /// <seealso cref="PrepareAsync"/>
350 /// <seealso cref="Stop"/>
351 /// <seealso cref="Pause"/>
352 /// <seealso cref="PlaybackCompleted"/>
353 /// <seealso cref="ApplyAudioStreamPolicy"/>
354 public virtual void Start()
356 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
357 if (State == PlayerState.Playing)
359 Log.Warn(PlayerLog.Tag, "playing state already");
362 ValidatePlayerState(PlayerState.Ready, PlayerState.Paused);
364 NativePlayer.Start(Handle).ThrowIfFailed("Failed to start the player");
365 Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
369 /// Stops playing media content.
372 /// The player must be in the <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.
373 /// It has no effect if the player is already in the <see cref="PlayerState.Ready"/> state.
375 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
376 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
377 /// <seealso cref="Start"/>
378 /// <seealso cref="Pause"/>
379 public virtual void Stop()
381 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
382 if (State == PlayerState.Ready)
384 Log.Warn(PlayerLog.Tag, "ready state already");
387 ValidatePlayerState(PlayerState.Paused, PlayerState.Playing);
389 NativePlayer.Stop(Handle).ThrowIfFailed("Failed to stop the player");
390 Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
394 /// Pauses the player.
397 /// The player must be in the <see cref="PlayerState.Playing"/> state.
398 /// It has no effect if the player is already in the <see cref="PlayerState.Paused"/> state.
400 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
401 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
402 /// <seealso cref="Start"/>
403 public virtual void Pause()
405 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
406 if (State == PlayerState.Paused)
408 Log.Warn(PlayerLog.Tag, "pause state already");
412 ValidatePlayerState(PlayerState.Playing);
414 NativePlayer.Pause(Handle).ThrowIfFailed("Failed to pause the player");
415 Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
418 private MediaSource _source;
421 /// Sets a media source for the player.
423 /// <param name="source">A <see cref="MediaSource"/> that specifies the source for playback.</param>
424 /// <remarks>The player must be in the <see cref="PlayerState.Idle"/> state.</remarks>
425 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
426 /// <exception cref="InvalidOperationException">
427 /// The player is not in the valid state.\n
429 /// It is not able to assign the source to the player.
431 /// <seealso cref="PrepareAsync"/>
432 public void SetSource(MediaSource source)
434 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
435 ValidatePlayerState(PlayerState.Idle);
439 source.AttachTo(this);
444 _source.DetachFrom(this);
448 Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
452 /// Captures a video frame asynchronously.
454 /// <returns>A task that represents the asynchronous capture operation.</returns>
455 /// <feature>http://tizen.org/feature/multimedia.raw_video</feature>
456 /// <remarks>The player must be in the <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.</remarks>
457 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
458 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
459 /// <exception cref="NotSupportedException">The required feature is not supported.</exception>
460 public async Task<CapturedFrame> CaptureVideoAsync()
462 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
464 ValidationUtil.ValidateFeatureSupported(Features.RawVideo);
466 ValidatePlayerState(PlayerState.Playing, PlayerState.Paused);
468 TaskCompletionSource<CapturedFrame> t = new TaskCompletionSource<CapturedFrame>();
470 NativePlayer.VideoCaptureCallback cb = (data, width, height, size, _) =>
472 Debug.Assert(size <= int.MaxValue);
474 byte[] buf = new byte[size];
475 Marshal.Copy(data, buf, 0, (int)size);
477 t.TrySetResult(new CapturedFrame(buf, width, height));
480 using (var cbKeeper = ObjectKeeper.Get(cb))
482 NativePlayer.CaptureVideo(Handle, cb)
483 .ThrowIfFailed("Failed to capture the video");
490 /// Gets the play position in milliseconds.
492 /// <remarks>The player must be in the <see cref="PlayerState.Ready"/>, <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.</remarks>
493 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
494 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
495 /// <seealso cref="SetPlayPositionAsync(int, bool)"/>
496 public int GetPlayPosition()
498 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
499 ValidatePlayerState(PlayerState.Ready, PlayerState.Paused, PlayerState.Playing);
501 int playPosition = 0;
503 NativePlayer.GetPlayPosition(Handle, out playPosition).
504 ThrowIfFailed("Failed to get the play position of the player");
506 Log.Info(PlayerLog.Tag, "get play position : " + playPosition);
511 private void SetPlayPosition(int milliseconds, bool accurate,
512 NativePlayer.SeekCompletedCallback cb)
514 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
515 var ret = NativePlayer.SetPlayPosition(Handle, milliseconds, accurate, cb, IntPtr.Zero);
517 //Note that we assume invalid param error is returned only when the position value is invalid.
518 if (ret == PlayerErrorCode.InvalidArgument)
520 throw new ArgumentOutOfRangeException(nameof(milliseconds), milliseconds,
521 "The position is not valid.");
523 if (ret != PlayerErrorCode.None)
525 Log.Error(PlayerLog.Tag, "Failed to set play position, " + (PlayerError)ret);
527 ret.ThrowIfFailed("Failed to set play position");
531 /// Sets the seek position for playback, asynchronously.
533 /// <param name="position">The value indicating a desired position in milliseconds.</param>
534 /// <param name="accurate">The value indicating whether the operation performs with accuracy.</param>
536 /// <para>The player must be in the <see cref="PlayerState.Ready"/>, <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.</para>
537 /// <para>If the <paramref name="accurate"/> is true, the play position will be adjusted as the specified <paramref name="position"/> value,
538 /// but this might be considerably slow. If false, the play position will be a nearest keyframe position.</para>
540 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
541 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
542 /// <exception cref="ArgumentOutOfRangeException">The specified position is not valid.</exception>
543 /// <seealso cref="GetPlayPosition"/>
544 public async Task SetPlayPositionAsync(int position, bool accurate)
546 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
547 ValidatePlayerState(PlayerState.Ready, PlayerState.Playing, PlayerState.Paused);
549 var taskCompletionSource = new TaskCompletionSource<bool>();
551 bool immediateResult = _source is MediaStreamSource;
553 NativePlayer.SeekCompletedCallback cb = _ => taskCompletionSource.TrySetResult(true);
555 using (var cbKeeper = ObjectKeeper.Get(cb))
557 SetPlayPosition(position, accurate, cb);
560 taskCompletionSource.TrySetResult(true);
563 await taskCompletionSource.Task;
566 Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
570 /// Sets playback rate.
572 /// <param name="rate">The value for the playback rate. Valid range is -5.0 to 5.0, inclusive.</param>
574 /// <para>The player must be in the <see cref="PlayerState.Ready"/>, <see cref="PlayerState.Playing"/> or <see cref="PlayerState.Paused"/> state.</para>
575 /// <para>The sound will be muted, when the playback rate is under 0.0 or over 2.0.</para>
577 /// <exception cref="ObjectDisposedException">The player has already been disposed of.</exception>
578 /// <exception cref="InvalidOperationException">
579 /// The player is not in the valid state.\n
581 /// Streaming playback.
583 /// <exception cref="ArgumentOutOfRangeException">
584 /// <paramref name="rate"/> is less than 5.0.\n
586 /// <paramref name="rate"/> is greater than 5.0.\n
588 /// <paramref name="rate"/> is zero.
590 public void SetPlaybackRate(float rate)
592 Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
593 if (rate < -5.0F || 5.0F < rate || rate == 0.0F)
595 throw new ArgumentOutOfRangeException(nameof(rate), rate, "Valid range is -5.0 to 5.0 (except 0.0)");
598 ValidatePlayerState(PlayerState.Ready, PlayerState.Playing, PlayerState.Paused);
600 NativePlayer.SetPlaybackRate(Handle, rate).ThrowIfFailed("Failed to set the playback rate.");
601 Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
605 /// Applies the audio stream policy.
607 /// <param name="policy">The <see cref="AudioStreamPolicy"/> to apply.</param>
609 /// The player must be in the <see cref="PlayerState.Idle"/> state.\n
611 /// <see cref="Player"/> does not support all <see cref="AudioStreamType"/>.\n
612 /// Supported types are <see cref="AudioStreamType.Media"/>, <see cref="AudioStreamType.System"/>,
613 /// <see cref="AudioStreamType.Alarm"/>, <see cref="AudioStreamType.Notification"/>,
614 /// <see cref="AudioStreamType.Emergency"/>, <see cref="AudioStreamType.VoiceInformation"/>,
615 /// <see cref="AudioStreamType.RingtoneVoip"/> and <see cref="AudioStreamType.MediaExternalOnly"/>.
617 /// <exception cref="ObjectDisposedException">
618 /// The player has already been disposed of.\n
620 /// <paramref name="policy"/> has already been disposed of.
622 /// <exception cref="InvalidOperationException">The player is not in the valid state.</exception>
623 /// <exception cref="ArgumentNullException"><paramref name="policy"/> is null.</exception>
624 /// <exception cref="NotSupportedException">
625 /// <see cref="AudioStreamType"/> of <paramref name="policy"/> is not supported by <see cref="Player"/>.
627 /// <seealso cref="AudioStreamPolicy"/>
628 public void ApplyAudioStreamPolicy(AudioStreamPolicy policy)
632 throw new ArgumentNullException(nameof(policy));
635 ValidatePlayerState(PlayerState.Idle);
637 NativePlayer.SetAudioPolicyInfo(Handle, policy.Handle).
638 ThrowIfFailed("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);
664 /// This method supports the product infrastructure and is not intended to be used directly from application code.
666 protected static Exception GetException(int errorCode, string message) =>
667 ((PlayerErrorCode)errorCode).GetException(message);