/* * 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); } } }