/* * Copyright (c) 2016 Samsung Electronics Co., Ltd All Rights Reserved * * Licensed under the Apache License, Version 2.0 (the License); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an AS IS BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Threading.Tasks; using System.Runtime.InteropServices; using System.Diagnostics; using System.IO; using System.Threading; using static Interop; namespace Tizen.Multimedia { internal static class PlayerLog { internal const string Tag = "Tizen.Multimedia.Player"; } /// /// Provides the ability to control media playback. /// /// /// The player provides functions to play a media content. /// It also provides functions to adjust the configurations of the player such as playback rate, volume, looping etc. /// Note that only one video player can be played at one time. /// public partial class Player : IDisposable, IDisplayable { private PlayerHandle _handle; /// /// Initializes a new instance of the class. /// public Player() { NativePlayer.Create(out _handle).ThrowIfFailed("Failed to create player"); Debug.Assert(_handle != null); RetrieveProperties(); if (Features.IsSupported(Features.AudioEffect)) { _audioEffect = new AudioEffect(this); } if (Features.IsSupported(Features.RawVideo)) { RegisterVideoFrameDecodedCallback(); } DisplaySettings = PlayerDisplaySettings.Create(this); } internal void ValidatePlayerState(params PlayerState[] desiredStates) { Debug.Assert(desiredStates.Length > 0); ValidateNotDisposed(); var curState = State; if (curState.IsAnyOf(desiredStates)) { return; } throw new InvalidOperationException($"The player is not in a valid state. " + $"Current State : { curState }, Valid State : { string.Join(", ", desiredStates) }."); } #region Dispose support private bool _disposed; /// /// Releases all resources used by the current instance. /// public void Dispose() { Dispose(true); } private void Dispose(bool disposing) { if (!_disposed) { ReplaceDisplay(null); if (_source != null) { try { _source.DetachFrom(this); } catch (Exception e) { Log.Error(PlayerLog.Tag, e.ToString()); } } _source = null; if (_handle != null) { _handle.Dispose(); } _disposed = true; } } internal void ValidateNotDisposed() { if (_disposed) { Log.Warn(PlayerLog.Tag, "player was disposed"); throw new ObjectDisposedException(nameof(Player)); } } internal bool IsDisposed => _disposed; #endregion #region Methods /// /// Gets the streaming download progress. /// /// The containing current download progress. /// The player must be in the or state. /// /// The player is not streaming.\n /// -or-\n /// The player is not in the valid state. /// /// The player has already been disposed of. public DownloadProgress GetDownloadProgress() { ValidatePlayerState(PlayerState.Playing, PlayerState.Paused); int start = 0; int current = 0; NativePlayer.GetStreamingDownloadProgress(Handle, out start, out current). ThrowIfFailed("Failed to get download progress"); Log.Info(PlayerLog.Tag, "get download progress : " + start + ", " + current); return new DownloadProgress(start, current); } /// /// Sets the subtitle path for playback. /// /// Only MicroDVD/SubViewer(*.sub), SAMI(*.smi), and SubRip(*.srt) subtitle formats are supported. /// The mediastorage privilege(http://tizen.org/privilege/mediastorage) must be added if any files are used to play located in the internal storage. /// The externalstorage privilege(http://tizen.org/privilege/externalstorage) must be added if any files are used to play located in the external storage. /// /// The player has already been disposed of. /// is an empty string. /// The specified path does not exist. /// The path is null. public void SetSubtitle(string path) { ValidateNotDisposed(); if (path == null) { throw new ArgumentNullException(nameof(path)); } if (path.Length == 0) { throw new ArgumentException("The path is empty.", nameof(path)); } if (!File.Exists(path)) { throw new FileNotFoundException($"The specified file does not exist.", path); } NativePlayer.SetSubtitlePath(Handle, path). ThrowIfFailed("Failed to set the subtitle path to the player"); } /// /// Removes the subtitle path. /// /// The player must be in the state. /// The player has already been disposed of. /// The player is not in the valid state. public void ClearSubtitle() { ValidatePlayerState(PlayerState.Idle); NativePlayer.SetSubtitlePath(Handle, null). ThrowIfFailed("Failed to clear the subtitle of the player"); } /// /// Sets the offset for the subtitle. /// /// The value indicating a desired offset in milliseconds. /// The player must be in the or state. /// The player has already been disposed of. /// /// The player is not in the valid state.\n /// -or-\n /// No subtitle is set. /// /// public void SetSubtitleOffset(int offset) { ValidatePlayerState(PlayerState.Playing, PlayerState.Paused); var err = NativePlayer.SetSubtitlePositionOffset(Handle, offset); if (err == PlayerErrorCode.FeatureNotSupported) { throw new InvalidOperationException("No subtitle set"); } err.ThrowIfFailed("Failed to the subtitle offset of the player"); } private void Prepare() { NativePlayer.Prepare(Handle).ThrowIfFailed("Failed to prepare the player"); } /// /// Called when the is invoked. /// protected virtual void OnPreparing() { RegisterEvents(); } /// /// Prepares the media player for playback, asynchronously. /// /// A task that represents the asynchronous prepare operation. /// To prepare the player, the player must be in the state, /// and a source must be set. /// No source is set. /// The player has already been disposed of. /// The player is not in the valid state. public virtual Task PrepareAsync() { if (_source == null) { throw new InvalidOperationException("No source is set."); } ValidatePlayerState(PlayerState.Idle); OnPreparing(); var completionSource = new TaskCompletionSource(); SetPreparing(); Task.Run(() => { try { Prepare(); ClearPreparing(); completionSource.SetResult(true); } catch (Exception e) { ClearPreparing(); completionSource.TrySetException(e); } }); return completionSource.Task; } /// /// Unprepares the player. /// /// /// The most recently used source is reset and is no longer associated with the player. Playback is no longer possible. /// If you want to use the player again, you have to set a source and call again. /// /// The player must be in the , , or state. /// It has no effect if the player is already in the state. /// /// /// The player has already been disposed of. /// The player is not in the valid state. public virtual void Unprepare() { if (State == PlayerState.Idle) { Log.Warn(PlayerLog.Tag, "idle state already"); return; } ValidatePlayerState(PlayerState.Ready, PlayerState.Paused, PlayerState.Playing); NativePlayer.Unprepare(Handle).ThrowIfFailed("Failed to unprepare the player"); OnUnprepared(); } /// /// Called after the is unprepared. /// /// protected virtual void OnUnprepared() { _source?.DetachFrom(this); _source = null; } /// /// Starts or resumes playback. /// /// /// The player must be in the or state. /// It has no effect if the player is already in the state.\n /// \n /// Sound can be mixed with other sounds if you don't control the stream focus using . /// /// The player has already been disposed of. /// The player is not in the valid state. /// /// /// /// /// public virtual void Start() { if (State == PlayerState.Playing) { Log.Warn(PlayerLog.Tag, "playing state already"); return; } ValidatePlayerState(PlayerState.Ready, PlayerState.Paused); NativePlayer.Start(Handle).ThrowIfFailed("Failed to start the player"); } /// /// Stops playing the media content. /// /// /// The player must be in the or state. /// It has no effect if the player is already in the state. /// /// The player has already been disposed of. /// The player is not in the valid state. /// /// public virtual void Stop() { if (State == PlayerState.Ready) { Log.Warn(PlayerLog.Tag, "ready state already"); return; } ValidatePlayerState(PlayerState.Paused, PlayerState.Playing); NativePlayer.Stop(Handle).ThrowIfFailed("Failed to stop the player"); } /// /// Pauses the player. /// /// /// The player must be in the state. /// It has no effect if the player is already in the state. /// /// The player has already been disposed of. /// The player is not in the valid state. /// public virtual void Pause() { if (State == PlayerState.Paused) { Log.Warn(PlayerLog.Tag, "pause state already"); return; } ValidatePlayerState(PlayerState.Playing); NativePlayer.Pause(Handle).ThrowIfFailed("Failed to pause the player"); } private MediaSource _source; /// /// Sets a media source for the player. /// /// A that specifies the source for playback. /// The player must be in the state. /// The player has already been disposed of. /// /// The player is not in the valid state.\n /// -or-\n /// It is not able to assign the source to the player. /// /// public void SetSource(MediaSource source) { ValidatePlayerState(PlayerState.Idle); if (source != null) { source.AttachTo(this); } if (_source != null) { _source.DetachFrom(this); } _source = source; } /// /// Captures a video frame, asynchronously. /// /// A task that represents the asynchronous capture operation. /// http://tizen.org/feature/multimedia.raw_video /// The player must be in the or state. /// The player has already been disposed of. /// The player is not in the valid state. /// The required feature is not supported. public async Task CaptureVideoAsync() { ValidationUtil.ValidateFeatureSupported(Features.RawVideo); ValidatePlayerState(PlayerState.Playing, PlayerState.Paused); TaskCompletionSource t = new TaskCompletionSource(); NativePlayer.VideoCaptureCallback cb = (data, width, height, size, _) => { Debug.Assert(size <= int.MaxValue); byte[] buf = new byte[size]; Marshal.Copy(data, buf, 0, (int)size); t.TrySetResult(new CapturedFrame(buf, width, height)); }; using (var cbKeeper = ObjectKeeper.Get(cb)) { NativePlayer.CaptureVideo(Handle, cb) .ThrowIfFailed("Failed to capture the video"); return await t.Task; } } /// /// Gets the play position in milliseconds. /// /// The player must be in the , , /// or state. /// The player has already been disposed of. /// The player is not in the valid state. /// public int GetPlayPosition() { ValidatePlayerState(PlayerState.Ready, PlayerState.Paused, PlayerState.Playing); int playPosition = 0; NativePlayer.GetPlayPosition(Handle, out playPosition). ThrowIfFailed("Failed to get the play position of the player"); Log.Info(PlayerLog.Tag, "get play position : " + playPosition); return playPosition; } private void SetPlayPosition(int milliseconds, bool accurate, NativePlayer.SeekCompletedCallback cb) { var ret = NativePlayer.SetPlayPosition(Handle, milliseconds, accurate, cb, IntPtr.Zero); //Note that we assume invalid param error is returned only when the position value is invalid. if (ret == PlayerErrorCode.InvalidArgument) { throw new ArgumentOutOfRangeException(nameof(milliseconds), milliseconds, "The position is not valid."); } if (ret != PlayerErrorCode.None) { Log.Error(PlayerLog.Tag, "Failed to set play position, " + (PlayerError)ret); } ret.ThrowIfFailed("Failed to set play position"); } /// /// Sets the seek position for playback, asynchronously. /// /// The value indicating a desired position in milliseconds. /// The value indicating whether the operation performs with accuracy. /// /// The player must be in the , , /// or state. /// If the is true, the play position will be adjusted as the specified value, /// but this might be considerably slow. If false, the play position will be a nearest keyframe position. /// /// The player has already been disposed of. /// The player is not in the valid state. /// The specified position is not valid. /// public async Task SetPlayPositionAsync(int position, bool accurate) { ValidatePlayerState(PlayerState.Ready, PlayerState.Playing, PlayerState.Paused); var taskCompletionSource = new TaskCompletionSource(); bool immediateResult = _source is MediaStreamSource; NativePlayer.SeekCompletedCallback cb = _ => taskCompletionSource.TrySetResult(true); using (var cbKeeper = ObjectKeeper.Get(cb)) { SetPlayPosition(position, accurate, cb); if (immediateResult) { taskCompletionSource.TrySetResult(true); } await taskCompletionSource.Task; } } /// /// Sets the playback rate. /// /// The value for the playback rate. Valid range is -5.0 to 5.0, inclusive. /// /// The player must be in the , , /// or state. /// The sound will be muted, when the playback rate is under 0.0 or over 2.0. /// /// The player has already been disposed of. /// /// The player is not in the valid state.\n /// -or-\n /// Streaming playback. /// /// /// is less than 5.0.\n /// -or-\n /// is greater than 5.0.\n /// -or-\n /// is zero. /// public void SetPlaybackRate(float rate) { if (rate < -5.0F || 5.0F < rate || rate == 0.0F) { throw new ArgumentOutOfRangeException(nameof(rate), rate, "Valid range is -5.0 to 5.0 (except 0.0)"); } ValidatePlayerState(PlayerState.Ready, PlayerState.Playing, PlayerState.Paused); NativePlayer.SetPlaybackRate(Handle, rate).ThrowIfFailed("Failed to set the playback rate."); } /// /// Applies the audio stream policy. /// /// The to apply. /// /// The player must be in the state.\n /// \n /// does not support all .\n /// Supported types are , , /// , , /// , , /// and . /// /// /// The player has already been disposed of.\n /// -or-\n /// has already been disposed of. /// /// The player is not in the valid state. /// is null. /// /// of is not supported by . /// /// public void ApplyAudioStreamPolicy(AudioStreamPolicy policy) { if (policy == null) { throw new ArgumentNullException(nameof(policy)); } ValidatePlayerState(PlayerState.Idle); NativePlayer.SetAudioPolicyInfo(Handle, policy.Handle). ThrowIfFailed("Failed to set the audio stream policy to the player"); } #endregion #region Preparing state private int _isPreparing; private bool IsPreparing() { return Interlocked.CompareExchange(ref _isPreparing, 1, 1) == 1; } private void SetPreparing() { Interlocked.Exchange(ref _isPreparing, 1); } private void ClearPreparing() { Interlocked.Exchange(ref _isPreparing, 0); } #endregion /// /// This method supports the product infrastructure and is not intended to be used directly from application code. /// protected static Exception GetException(int errorCode, string message) => ((PlayerErrorCode)errorCode).GetException(message); } }