/*
* 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
{
static internal class PlayerLog
{
internal const string Tag = "Tizen.Multimedia.Player";
internal const string Enter = "[ENTER]";
internal const string Leave = "[LEAVE]";
}
///
/// 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 class Player : IDisposable, IDisplayable
{
private PlayerHandle _handle;
///
/// Occurs when playback of a media is finished.
///
public event EventHandler PlaybackCompleted;
private NativePlayer.PlaybackCompletedCallback _playbackCompletedCallback;
///
/// Occurs when playback of a media is interrupted.
///
public event EventHandler PlaybackInterrupted;
private NativePlayer.PlaybackInterruptedCallback _playbackInterruptedCallback;
///
/// Occurs when any error occurs.
///
/// The event handler will be executed on an internal thread.
public event EventHandler ErrorOccurred;
private NativePlayer.PlaybackErrorCallback _playbackErrorCallback;
///
/// Occurs when the video stream changed.
///
/// The event handler will be executed on an internal thread.
public event EventHandler VideoStreamChanged;
private NativePlayer.VideoStreamChangedCallback _videoStreamChangedCallback;
///
/// Occurs when the subtitle is updated.
///
/// The event handler will be executed on an internal thread.
public event EventHandler SubtitleUpdated;
private NativePlayer.SubtitleUpdatedCallback _subtitleUpdatedCallback;
///
/// Occurs when there is a change in the buffering status of streaming.
///
public event EventHandler BufferingProgressChanged;
private NativePlayer.BufferingProgressCallback _bufferingProgressCallback;
internal event EventHandler MediaStreamAudioBufferStatusChanged;
private NativePlayer.MediaStreamBufferStatusCallback _mediaStreamAudioBufferStatusChangedCallback;
internal event EventHandler MediaStreamVideoBufferStatusChanged;
private NativePlayer.MediaStreamBufferStatusCallback _mediaStreamVideoBufferStatusChangedCallback;
internal event EventHandler MediaStreamAudioSeekingOccurred;
private NativePlayer.MediaStreamSeekCallback _mediaStreamAudioSeekCallback;
internal event EventHandler MediaStreamVideoSeekingOccurred;
private NativePlayer.MediaStreamSeekCallback _mediaStreamVideoSeekCallback;
///
/// Initialize a new instance of the Player class.
///
public Player()
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
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);
}
private void RetrieveProperties()
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
NativePlayer.GetAudioLatencyMode(Handle, out _audioLatencyMode).
ThrowIfFailed("Failed to initialize the player");
NativePlayer.IsLooping(Handle, out _isLooping).ThrowIfFailed("Failed to initialize the player");
Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
}
private bool _callbackRegistered;
private void RegisterCallbacks()
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
if (_callbackRegistered)
{
return;
}
RegisterSubtitleUpdatedCallback();
RegisterErrorOccurredCallback();
RegisterPlaybackInterruptedCallback();
RegisterVideoStreamChangedCallback();
RegisterBufferingCallback();
RegisterMediaStreamBufferStatusCallback();
RegisterMediaStreamSeekCallback();
RegisterPlaybackCompletedCallback();
_callbackRegistered = true;
}
///
/// Gets the native handle of the player.
///
/// An IntPtr that contains the native handle of the player.
/// The player has already been disposed of.
public IntPtr Handle
{
get
{
ValidateNotDisposed();
return _handle.DangerousGetHandle();
}
}
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 Properties
#region Network configuration
private string _cookie = "";
private string _userAgent = "";
///
/// Gets or Sets the cookie for streaming playback.
///
/// To set, the player must be in the state.
/// The player is not in the valid state.
/// The player has already been disposed of.
/// The value to set is null.
public string Cookie
{
get
{
Log.Info(PlayerLog.Tag, "get cookie : " + _cookie);
return _cookie;
}
set
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
ValidatePlayerState(PlayerState.Idle);
if (value == null)
{
Log.Error(PlayerLog.Tag, "cookie can't be null");
throw new ArgumentNullException(nameof(value), "Cookie can't be null.");
}
NativePlayer.SetStreamingCookie(Handle, value, value.Length).
ThrowIfFailed("Failed to set the cookie to the player");
_cookie = value;
Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
}
}
///
/// Gets or Sets the user agent for streaming playback.
///
/// To set, the player must be in the state.
/// The player is not in the valid state.
/// The player has already been disposed of.
/// The value to set is null.
public string UserAgent
{
get
{
Log.Info(PlayerLog.Tag, "get useragent : " + _userAgent);
return _userAgent;
}
set
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
ValidatePlayerState(PlayerState.Idle);
if (value == null)
{
Log.Error(PlayerLog.Tag, "UserAgent can't be null");
throw new ArgumentNullException(nameof(value), "UserAgent can't be null.");
}
NativePlayer.SetStreamingUserAgent(Handle, value, value.Length).
ThrowIfFailed("Failed to set the user agent to the player");
_userAgent = value;
Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
}
}
#endregion
///
/// Gets the state of the player.
///
/// The current state of the player.
/// The player has already been disposed of.
public PlayerState State
{
get
{
ValidateNotDisposed();
if (IsPreparing())
{
return PlayerState.Preparing;
}
int state = 0;
NativePlayer.GetState(Handle, out state).ThrowIfFailed("Failed to retrieve the state of the player");
Debug.Assert(Enum.IsDefined(typeof(PlayerState), state));
return (PlayerState)state;
}
}
private AudioLatencyMode _audioLatencyMode;
///
/// Gets or sets the audio latency mode.
///
/// A that specifies the mode. The default is .
///
/// If the mode is ,
/// audio output interval can be increased so, it can keep more audio data to play.
/// But, state transition like pause or resume can be more slower than default().
///
/// The player has already been disposed of.
/// The value is not valid.
public AudioLatencyMode AudioLatencyMode
{
get
{
Log.Info(PlayerLog.Tag, "get audio latency mode : " + _audioLatencyMode);
return _audioLatencyMode;
}
set
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
ValidateNotDisposed();
if (_audioLatencyMode == value)
{
return;
}
ValidationUtil.ValidateEnum(typeof(AudioLatencyMode), value);
NativePlayer.SetAudioLatencyMode(Handle, value).
ThrowIfFailed("Failed to set the audio latency mode of the player");
_audioLatencyMode = value;
}
}
private bool _isLooping;
///
/// Gets or sets the looping state.
///
/// true if the playback is looping; otherwise, false. The default value is false.
/// The player has already been disposed of.
public bool IsLooping
{
get
{
Log.Info(PlayerLog.Tag, "get looping : " + _isLooping);
return _isLooping;
}
set
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
ValidateNotDisposed();
if (_isLooping == value)
{
return;
}
NativePlayer.SetLooping(Handle, value).ThrowIfFailed("Failed to set the looping state of the player");
_isLooping = value;
}
}
#region Display methods
///
/// Gets the display settings.
///
/// A that specifies the display settings.
public PlayerDisplaySettings DisplaySettings { get; }
private Display _display;
private PlayerErrorCode SetDisplay(Display display)
{
if (display == null)
{
Log.Info(PlayerLog.Tag, "set display to none");
return NativePlayer.SetDisplay(Handle, DisplayType.None, IntPtr.Zero);
}
return display.ApplyTo(this);
}
private void ReplaceDisplay(Display newDisplay)
{
_display?.SetOwner(null);
_display = newDisplay;
_display?.SetOwner(this);
}
///
/// Gets or sets the display.
///
/// A that specifies the display.
/// The player must be in the state.
/// The player has already been disposed of.
/// The value has already been assigned to another player.
/// The player is not in the valid state.
public Display Display
{
get
{
return _display;
}
set
{
ValidatePlayerState(PlayerState.Idle);
if (value?.Owner != null)
{
if (ReferenceEquals(this, value.Owner))
{
return;
}
throw new ArgumentException("The display has already been assigned to another.");
}
SetDisplay(value).ThrowIfFailed("Failed to set the display to the player");
ReplaceDisplay(value);
}
}
PlayerErrorCode IDisplayable.ApplyEvasDisplay(DisplayType type, ElmSharp.EvasObject evasObject)
{
Debug.Assert(IsDisposed == false);
Debug.Assert(Enum.IsDefined(typeof(DisplayType), type));
return NativePlayer.SetDisplay(Handle, type, evasObject);
}
#endregion
private PlayerTrackInfo _audioTrack;
///
/// Gets the track info for audio.
///
/// A for audio.
public PlayerTrackInfo AudioTrackInfo
{
get
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
if (_audioTrack == null)
{
_audioTrack = new PlayerTrackInfo(this, StreamType.Audio);
}
return _audioTrack;
}
}
private PlayerTrackInfo _subtitleTrackInfo;
///
/// Gets the track info for subtitle.
///
/// A for subtitle.
public PlayerTrackInfo SubtitleTrackInfo
{
get
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
if (_subtitleTrackInfo == null)
{
_subtitleTrackInfo = new PlayerTrackInfo(this, StreamType.Text);
}
return _subtitleTrackInfo;
}
}
private StreamInfo _streamInfo;
///
/// Gets the stream information.
///
/// A for this player.
public StreamInfo StreamInfo
{
get
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
if (_streamInfo == null)
{
_streamInfo = new StreamInfo(this);
}
return _streamInfo;
}
}
private readonly AudioEffect _audioEffect;
///
/// Gets the audio effect.
///
/// http://tizen.org/feature/multimedia.custom_audio_effect
/// The required feature is not supported.
public AudioEffect AudioEffect
{
get
{
if (_audioEffect == null)
{
throw new NotSupportedException($"The feature({Features.AudioEffect}) is not supported.");
}
return _audioEffect;
}
}
#endregion
#region Dispose support
private bool _disposed;
///
/// Releases all resources used by the current instance.
///
public void Dispose()
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
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 or sets the mute state.
///
/// true if the player is muted; otherwise, false.
/// The player has already been disposed of.
public bool Muted
{
get
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
bool value = false;
NativePlayer.IsMuted(Handle, out value).ThrowIfFailed("Failed to get the mute state of the player");
Log.Info(PlayerLog.Tag, "get mute : " + value);
return value;
}
set
{
NativePlayer.SetMute(Handle, value).ThrowIfFailed("Failed to set the mute state of the player");
}
}
///
/// 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()
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
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);
}
#region Volume
///
/// Gets or sets the current volume.
///
/// Valid volume range is from 0 to 1.0, inclusive.
/// The player has already been disposed of.
///
/// is less than zero.\n
/// -or-\n
/// is greater than 1.0.
///
public float Volume
{
get
{
float value = 0.0F;
NativePlayer.GetVolume(Handle, out value, out value).
ThrowIfFailed("Failed to get the volume of the player");
return value;
}
set
{
if (value < 0F || 1.0F < value)
{
throw new ArgumentOutOfRangeException(nameof(value), value,
$"Valid volume range is 0 <= value <= 1.0, but got { value }.");
}
NativePlayer.SetVolume(Handle, value, value).
ThrowIfFailed("Failed to set the volume of the player");
}
}
#endregion
///
/// 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)
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
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");
Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
}
///
/// 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()
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
ValidatePlayerState(PlayerState.Idle);
NativePlayer.SetSubtitlePath(Handle, null).
ThrowIfFailed("Failed to clear the subtitle of the player");
Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
}
///
/// 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)
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
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");
Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
}
private void Prepare()
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
NativePlayer.Prepare(Handle).ThrowIfFailed("Failed to prepare the player");
}
protected virtual void OnPreparing()
{
RegisterCallbacks();
}
///
/// 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()
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
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);
}
});
Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
return completionSource.Task;
}
///
/// Unprepares the player.
///
///
/// The most recently used source is reset and 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()
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
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();
}
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()
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
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");
Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
}
///
/// Stops playing 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()
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
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");
Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
}
///
/// 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()
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
if (State == PlayerState.Paused)
{
Log.Warn(PlayerLog.Tag, "pause state already");
return;
}
ValidatePlayerState(PlayerState.Playing);
NativePlayer.Pause(Handle).ThrowIfFailed("Failed to pause the player");
Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
}
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)
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
ValidatePlayerState(PlayerState.Idle);
if (source != null)
{
source.AttachTo(this);
}
if (_source != null)
{
_source.DetachFrom(this);
}
_source = source;
Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
}
///
/// 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()
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
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()
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
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)
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
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)
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
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;
}
Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
}
///
/// Sets 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)
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
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.");
Log.Debug(PlayerLog.Tag, PlayerLog.Leave);
}
///
/// Applies the audio stream policy.
///
/// The to apply.
/// The player must be in the state.
///
/// 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.
public void ApplyAudioStreamPolicy(AudioStreamPolicy policy)
{
Log.Debug(PlayerLog.Tag, PlayerLog.Enter);
if (policy == null)
{
throw new ArgumentNullException(nameof(policy));
}
if (policy.Handle == IntPtr.Zero)
{
throw new ObjectDisposedException(nameof(policy));
}
ValidatePlayerState(PlayerState.Idle);
NativePlayer.SetAudioPolicyInfo(Handle, policy.Handle).
ThrowIfFailed("Failed to set the audio stream policy to the player");
}
#endregion
#region Callback registrations
private void RegisterSubtitleUpdatedCallback()
{
_subtitleUpdatedCallback = (duration, text, _) =>
{
Log.Debug(PlayerLog.Tag, "duration : " + duration + ", text : " + text);
SubtitleUpdated?.Invoke(this, new SubtitleUpdatedEventArgs(duration, text));
};
NativePlayer.SetSubtitleUpdatedCb(Handle, _subtitleUpdatedCallback).
ThrowIfFailed("Failed to initialize the player");
}
private void RegisterPlaybackCompletedCallback()
{
_playbackCompletedCallback = _ =>
{
Log.Debug(PlayerLog.Tag, "completed callback");
PlaybackCompleted?.Invoke(this, EventArgs.Empty);
};
NativePlayer.SetCompletedCb(Handle, _playbackCompletedCallback).
ThrowIfFailed("Failed to set PlaybackCompleted");
}
private void RegisterPlaybackInterruptedCallback()
{
_playbackInterruptedCallback = (code, _) =>
{
if (!Enum.IsDefined(typeof(PlaybackInterruptionReason), code))
{
return;
}
if (code == PlaybackInterruptionReason.ResourceConflict)
{
OnUnprepared();
}
Log.Warn(PlayerLog.Tag, "interrupted reason : " + code);
PlaybackInterrupted?.Invoke(this, new PlaybackInterruptedEventArgs(code));
};
NativePlayer.SetInterruptedCb(Handle, _playbackInterruptedCallback).
ThrowIfFailed("Failed to set PlaybackInterrupted");
}
private void RegisterErrorOccurredCallback()
{
_playbackErrorCallback = (code, _) =>
{
//TODO handle service disconnected error.
Log.Warn(PlayerLog.Tag, "error code : " + code);
ErrorOccurred?.Invoke(this, new PlayerErrorOccurredEventArgs((PlayerError)code));
};
NativePlayer.SetErrorCb(Handle, _playbackErrorCallback).
ThrowIfFailed("Failed to set PlaybackError");
}
#region VideoFrameDecoded event
private EventHandler _videoFrameDecoded;
private NativePlayer.VideoFrameDecodedCallback _videoFrameDecodedCallback;
///
/// Occurs when a video frame is decoded.
///
///
/// The event handler will be executed on an internal thread.
/// The in event args should be disposed after use.
///
/// http://tizen.org/feature/multimedia.raw_video
/// The required feature is not supported.
///
public event EventHandler VideoFrameDecoded
{
add
{
ValidationUtil.ValidateFeatureSupported(Features.RawVideo);
_videoFrameDecoded += value;
}
remove
{
ValidationUtil.ValidateFeatureSupported(Features.RawVideo);
_videoFrameDecoded -= value;
}
}
private void RegisterVideoFrameDecodedCallback()
{
_videoFrameDecodedCallback = (packetHandle, _) =>
{
var handler = _videoFrameDecoded;
if (handler != null)
{
Log.Debug(PlayerLog.Tag, "packet : " + packetHandle);
handler.Invoke(this,
new VideoFrameDecodedEventArgs(MediaPacket.From(packetHandle)));
}
else
{
MediaPacket.From(packetHandle).Dispose();
}
};
NativePlayer.SetVideoFrameDecodedCb(Handle, _videoFrameDecodedCallback).
ThrowIfFailed("Failed to register the VideoFrameDecoded");
}
#endregion
private void RegisterVideoStreamChangedCallback()
{
ValidatePlayerState(PlayerState.Idle);
_videoStreamChangedCallback = (width, height, fps, bitrate, _) =>
{
Log.Debug(PlayerLog.Tag, "height : " + height + ", width : " + width
+ ", fps : " + fps + ", bitrate : " + bitrate);
VideoStreamChanged?.Invoke(this, new VideoStreamChangedEventArgs(height, width, fps, bitrate));
};
NativePlayer.SetVideoStreamChangedCb(Handle, _videoStreamChangedCallback).
ThrowIfFailed("Failed to set the video stream changed callback");
}
private void RegisterBufferingCallback()
{
_bufferingProgressCallback = (percent, _) =>
{
Log.Debug(PlayerLog.Tag, $"Buffering callback with percent { percent }");
BufferingProgressChanged?.Invoke(this, new BufferingProgressChangedEventArgs(percent));
};
NativePlayer.SetBufferingCb(Handle, _bufferingProgressCallback).
ThrowIfFailed("Failed to set BufferingProgress");
}
private void RegisterMediaStreamBufferStatusCallback()
{
_mediaStreamAudioBufferStatusChangedCallback = (status, _) =>
{
Debug.Assert(Enum.IsDefined(typeof(MediaStreamBufferStatus), status));
Log.Debug(PlayerLog.Tag, "audio buffer status : " + status);
MediaStreamAudioBufferStatusChanged?.Invoke(this,
new MediaStreamBufferStatusChangedEventArgs(status));
};
_mediaStreamVideoBufferStatusChangedCallback = (status, _) =>
{
Debug.Assert(Enum.IsDefined(typeof(MediaStreamBufferStatus), status));
Log.Debug(PlayerLog.Tag, "video buffer status : " + status);
MediaStreamVideoBufferStatusChanged?.Invoke(this,
new MediaStreamBufferStatusChangedEventArgs(status));
};
RegisterMediaStreamBufferStatusCallback(StreamType.Audio, _mediaStreamAudioBufferStatusChangedCallback);
RegisterMediaStreamBufferStatusCallback(StreamType.Video, _mediaStreamVideoBufferStatusChangedCallback);
}
private void RegisterMediaStreamBufferStatusCallback(StreamType streamType,
NativePlayer.MediaStreamBufferStatusCallback cb)
{
NativePlayer.SetMediaStreamBufferStatusCb(Handle, streamType, cb).
ThrowIfFailed("Failed to SetMediaStreamBufferStatus");
}
private void RegisterMediaStreamSeekCallback()
{
_mediaStreamAudioSeekCallback = (offset, _) =>
{
Log.Debug(PlayerLog.Tag, "audio seeking offset : " + offset);
MediaStreamAudioSeekingOccurred?.Invoke(this, new MediaStreamSeekingOccurredEventArgs(offset));
};
_mediaStreamVideoSeekCallback = (offset, _) =>
{
Log.Debug(PlayerLog.Tag, "video seeking offset : " + offset);
MediaStreamVideoSeekingOccurred?.Invoke(this, new MediaStreamSeekingOccurredEventArgs(offset));
};
RegisterMediaStreamSeekCallback(StreamType.Audio, _mediaStreamAudioSeekCallback);
RegisterMediaStreamSeekCallback(StreamType.Video, _mediaStreamVideoSeekCallback);
}
private void RegisterMediaStreamSeekCallback(StreamType streamType, NativePlayer.MediaStreamSeekCallback cb)
{
NativePlayer.SetMediaStreamSeekCb(Handle, streamType, cb).
ThrowIfFailed("Failed to SetMediaStreamSeek");
}
#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);
}
}