/*
* 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.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Native = Interop.ScreenMirroring;
namespace Tizen.Multimedia.Remoting
{
///
/// Provides the ability to connect to and disconnect from a screen mirroring source,
/// start, pause, and resume the screen mirroring as a sink.
///
public class ScreenMirroring : IDisposable, IDisplayable
{
private const string Feature = "http://tizen.org/feature/network.wifi.direct.display";
private const int Port = 2022;
private IntPtr _handle;
private AtomicState _state;
private bool _disposed = false;
internal IntPtr Handle
{
get
{
ThrowIfDisposed();
return _handle;
}
}
private static bool IsSupported()
{
return System.Information.TryGetValue(Feature, out bool isSupported) ? isSupported : false;
}
///
/// Initializes a new instance of the ScreenMirroring class.
///
/// http://tizen.org/feature/network.wifi.direct.display
/// The feature is not supported.
public ScreenMirroring()
{
if (IsSupported() == false)
{
throw new PlatformNotSupportedException($"The feature({Feature}) is not supported on the current device");
}
Native.Create(out _handle).ThrowIfError("Failed to create ScreenMirroring.");
_state = new AtomicState();
AudioInfo = new ScreenMirroringAudioInfo(this);
VideoInfo = new ScreenMirroringVideoInfo(this);
RegisterStateChangedEvent();
}
~ScreenMirroring()
{
Dispose(false);
}
///
/// Occurs when the state is changed.
///
public event EventHandler StateChanged;
///
/// Occurs when an error occurs.
///
public event EventHandler ErrorOccurred;
#region Display support
private Display _display;
private void DetachDisplay()
{
if (_display != null)
{
_display.SetOwner(null);
_display = null;
}
}
private void SetDisplay(Display display)
{
if (display == null)
{
throw new ArgumentNullException(nameof(Display));
}
display.SetOwner(this);
display.ApplyTo(this).ThrowIfError("Failed to set display.");
_display = display;
}
ScreenMirroringErrorCode IDisplayable.ApplyEvasDisplay(DisplayType type,
ElmSharp.EvasObject evasObject)
{
Debug.Assert(Enum.IsDefined(typeof(DisplayType), type));
return Native.SetDisplay(Handle, (int)type, evasObject);
}
ScreenMirroringErrorCode IDisplayable.ApplyEcoreWindow(IntPtr windowHandle)
{
throw new NotSupportedException("ScreenMirroring does not support NUI.Window display.");
}
#endregion
///
/// Gets the negotiated audio info.
///
public ScreenMirroringAudioInfo AudioInfo { get; }
///
/// Gets the negotiated video info.
///
public ScreenMirroringVideoInfo VideoInfo { get; }
private bool IsConnected
{
get
{
return _state.IsOneOf(ScreenMirroringState.Connected, ScreenMirroringState.Playing,
ScreenMirroringState.Paused);
}
}
internal void ThrowIfNotConnected()
{
ThrowIfDisposed();
if (IsConnected == false)
{
throw new InvalidOperationException("ScreenMirroring is not connected.");
}
}
///
/// Prepares the screen mirroring with the specified display.
///
///
/// The state must be .\n
/// \n
/// All supported resolutions will be candidates.
///
/// The display where the mirroring will be played on.
///
/// has already been assigned to another.
///
/// is null.
///
/// The current state is not in the valid.\n
/// -or-\n
/// An internal error occurs.
///
/// The has already been disposed.
public void Prepare(Display display)
{
PrepareCore(display, (ScreenMirroringResolutions)0);
}
///
/// Prepares the screen mirroring with the specified display and resolutions.
///
///
/// The state must be .
///
/// The display where the mirroring will be played on.
/// The desired resolutions.
///
/// contain invalid flags.\n
/// -or-\n
/// has already been assigned to another.
///
/// is null.
///
/// The current state is not in the valid.\n
/// -or-\n
/// An internal error occurs.
///
/// The has already been disposed.
public void Prepare(Display display, ScreenMirroringResolutions resolutions)
{
ValidationUtil.ValidateFlagsEnum(resolutions, (ScreenMirroringResolutions)((1 << 7) - 1), nameof(resolutions));
PrepareCore(display, resolutions);
}
private void PrepareCore(Display display, ScreenMirroringResolutions resolutions)
{
ValidateState(ScreenMirroringState.Idle);
Native.SetResolution(Handle, resolutions).ThrowIfError("Failed to set resolutions.");
try
{
SetDisplay(display);
Native.Prepare(Handle).ThrowIfError("Failed to prepare.");
}
catch
{
DetachDisplay();
throw;
}
}
///
/// Creates the connection and ready for receiving data from a mirroring source.
///
/// The source ip address to connect.
///
/// The state must be state by
/// .
///
/// A task that represents the asynchronous operation.
/// http://tizen.org/privilege/internet
/// is null.
///
/// The current state is not in the valid.\n
/// -or-\n
/// An internal error occurs.
///
/// is a zero-length string, contains only white space.
/// The has already been disposed.
/// Caller does not have required permission.
public Task ConnectAsync(string sourceIp)
{
if (sourceIp == null)
{
throw new ArgumentNullException(nameof(sourceIp));
}
if (string.IsNullOrWhiteSpace(sourceIp))
{
throw new ArgumentException($"{nameof(sourceIp)} is a zero-length string.", nameof(sourceIp));
}
ValidateState(ScreenMirroringState.Prepared);
Native.SetIpAndPort(Handle, sourceIp, Port.ToString()).ThrowIfError("Failed to set ip.");
var tcs = new TaskCompletionSource();
Task.Factory.StartNew(() =>
{
Native.Connect(Handle).ThrowIfError("Failed to connect");
tcs.SetResult(true);
});
return tcs.Task;
}
///
/// Starts mirroring from the source.
///
///
/// The state must be state by
/// .
///
/// A task that represents the asynchronous operation.
/// http://tizen.org/privilege/internet
///
/// The current state is not in the valid.\n
/// -or-\n
/// An internal error occurs.
///
/// The has already been disposed.
/// Caller does not have required permission.
public Task StartAsync()
{
ValidateState(ScreenMirroringState.Connected);
var tcs = new TaskCompletionSource();
Task.Factory.StartNew(() =>
{
Native.StartAsync(Handle).ThrowIfError("Failed to start.");
tcs.TrySetResult(true);
});
return tcs.Task;
}
///
/// Pauses mirroring from the source.
///
///
/// The state must be state by
/// .
///
/// A task that represents the asynchronous operation.
/// http://tizen.org/privilege/internet
///
/// The current state is not in the valid.\n
/// -or-\n
/// An internal error occurs.
///
/// The has already been disposed.
/// Caller does not have required permission.
public Task PauseAsync()
{
ValidateState(ScreenMirroringState.Playing);
var tcs = new TaskCompletionSource();
Task.Factory.StartNew(() =>
{
Native.PauseAsync(Handle).ThrowIfError("Failed to prepare.");
tcs.TrySetResult(true);
});
return tcs.Task;
}
///
/// Resumes mirroring from the source.
///
///
/// The state must be state by
/// .
///
/// A task that represents the asynchronous operation.
/// http://tizen.org/privilege/internet
///
/// The current state is not in the valid.\n
/// -or-\n
/// An internal error occurs.
///
/// The has already been disposed.
/// Caller does not have required permission.
public Task ResumeAsync()
{
ValidateState(ScreenMirroringState.Paused);
var tcs = new TaskCompletionSource();
Task.Factory.StartNew(() =>
{
Native.ResumeAsync(Handle).ThrowIfError("Failed to resume.");
tcs.TrySetResult(true);
});
return tcs.Task;
}
///
/// Disconnects from the source.
///
///
/// The state must be ,
/// or .
///
/// http://tizen.org/privilege/internet
///
/// The current state is not in the valid.\n
/// -or-\n
/// An internal error occurs.
///
/// The has already been disposed.
/// Caller does not have required permission.
public void Disconnect()
{
ValidateState(ScreenMirroringState.Connected, ScreenMirroringState.Playing,
ScreenMirroringState.Paused);
Native.Disconnect(Handle).ThrowIfError("Failed to disconnect.");
}
///
/// Unprepares the screen mirroring.
///
///
/// The state must be ,
/// or .
///
///
/// The current state is not in the valid.\n
/// -or-\n
/// An internal error occurs.
///
/// The has already been disposed.
public void Unprepare()
{
ValidateState(ScreenMirroringState.Prepared, ScreenMirroringState.Disconnected);
Native.Unprepare(Handle).ThrowIfError("Failed to reset.");
DetachDisplay();
}
private void ThrowIfDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(nameof(ScreenMirroring));
}
}
///
/// Releases all resource used by the object.
///
///
/// Call when you are finished using the .
/// The method leaves the in an unusable
/// state. After calling , you must release all references to the
/// so the garbage collector can reclaim the memory that the
/// was occupying.
///
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
///
/// Releases the resources used by the ScreenMirroring.
///
///
/// true to release both managed and unmanaged resources; false to release only unmanaged resources.
///
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
DetachDisplay();
if (_handle != IntPtr.Zero)
{
Native.Destroy(_handle);
_handle = IntPtr.Zero;
}
_disposed = true;
}
}
private Native.StateChangedCallback _stateChangedCallback;
private void RegisterStateChangedEvent()
{
_stateChangedCallback = (error, state, _) =>
{
var prevState = _state.Value;
_state.Value = state;
if (prevState != state)
{
StateChanged?.Invoke(this, new ScreenMirroringStateChangedEventArgs(state));
}
if (error != ScreenMirroringErrorCode.None)
{
ErrorOccurred?.Invoke(this, new ScreenMirroringErrorOccurredEventArgs(
ScreenMirroringError.InvalidOperation));
}
};
Native.SetStateChangedCb(Handle, _stateChangedCallback).
ThrowIfError("Failed to initialize StateChanged event.");
}
private void ValidateState(params ScreenMirroringState[] required)
{
Debug.Assert(required.Length > 0);
if (_disposed)
{
throw new ObjectDisposedException(nameof(ScreenMirroring));
}
var curState = _state.Value;
if (!required.Contains(curState))
{
throw new InvalidOperationException($"The screen mirroring is not in a valid state. " +
$"Current State : { curState }, Valid State : { string.Join(", ", required) }.");
}
}
}
internal class AtomicState
{
private int _value;
public AtomicState()
{
_value = (int)ScreenMirroringState.Idle;
}
public ScreenMirroringState Value
{
get
{
return (ScreenMirroringState)Interlocked.CompareExchange(ref _value, 0, 0);
}
set
{
Interlocked.Exchange(ref _value, (int)value);
}
}
public bool IsOneOf(params ScreenMirroringState[] states)
{
return states.Contains(Value);
}
}
}