6f08ffbfc4774275ffb9e5057a01d141d9d9c3c7
[platform/core/csapi/tizenfx.git] / src / Tizen.Multimedia.Remoting / ScreenMirroring / ScreenMirroring.cs
1 /*
2  * Copyright (c) 2016 Samsung Electronics Co., Ltd All Rights Reserved
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 using System;
18 using System.Diagnostics;
19 using System.Linq;
20 using System.Threading;
21 using System.Threading.Tasks;
22 using Native = Interop.ScreenMirroring;
23
24 namespace Tizen.Multimedia.Remoting
25 {
26     /// <summary>
27     /// Provides the ability to connect to and disconnect from a screen mirroring source,
28     /// start, pause, and resume the screen mirroring as a sink.
29     /// </summary>
30     public class ScreenMirroring : IDisposable, IDisplayable<ScreenMirroringErrorCode>
31     {
32         private const string Feature = "http://tizen.org/feature/network.wifi.direct.display";
33         private const int Port = 2022;
34
35         private IntPtr _handle;
36
37         private AtomicState _state;
38
39         private bool _disposed = false;
40
41         internal IntPtr Handle
42         {
43             get
44             {
45                 ThrowIfDisposed();
46
47                 return _handle;
48             }
49         }
50
51         private static bool IsSupported()
52         {
53             return System.Information.TryGetValue(Feature, out bool isSupported) ? isSupported : false;
54         }
55
56         /// <summary>
57         /// Initializes a new instance of the ScreenMirroring class.
58         /// </summary>
59         /// <feature>http://tizen.org/feature/network.wifi.direct.display</feature>
60         /// <exception cref="NotSupportedException">The feature is not supported.</exception>
61         public ScreenMirroring()
62         {
63             if (IsSupported() == false)
64             {
65                 throw new PlatformNotSupportedException($"The feature({Feature}) is not supported on the current device");
66             }
67
68             Native.Create(out _handle).ThrowIfError("Failed to create ScreenMirroring.");
69
70             _state = new AtomicState();
71
72             AudioInfo = new ScreenMirroringAudioInfo(this);
73             VideoInfo = new ScreenMirroringVideoInfo(this);
74
75             RegisterStateChangedEvent();
76         }
77
78         ~ScreenMirroring()
79         {
80             Dispose(false);
81         }
82
83         /// <summary>
84         /// Occurs when the state is changed.
85         /// </summary>
86         public event EventHandler<ScreenMirroringStateChangedEventArgs> StateChanged;
87
88         /// <summary>
89         /// Occurs when an error occurs.
90         /// </summary>
91         public event EventHandler<ScreenMirroringErrorOccurredEventArgs> ErrorOccurred;
92
93         #region Display support
94
95         private Display _display;
96
97         private void DetachDisplay()
98         {
99             if (_display != null)
100             {
101                 _display.SetOwner(null);
102                 _display = null;
103             }
104         }
105
106         private void SetDisplay(Display display)
107         {
108             if (display == null)
109             {
110                 throw new ArgumentNullException(nameof(Display));
111             }
112
113             display.SetOwner(this);
114             display.ApplyTo(this).ThrowIfError("Failed to set display.");
115
116             _display = display;
117         }
118
119         ScreenMirroringErrorCode IDisplayable<ScreenMirroringErrorCode>.ApplyEvasDisplay(DisplayType type,
120             ElmSharp.EvasObject evasObject)
121         {
122             Debug.Assert(Enum.IsDefined(typeof(DisplayType), type));
123
124             return Native.SetDisplay(Handle, (int)type, evasObject);
125         }
126
127         ScreenMirroringErrorCode IDisplayable<ScreenMirroringErrorCode>.ApplyEcoreWindow(IntPtr windowHandle)
128         {
129             throw new NotSupportedException("ScreenMirroring does not support NUI.Window display.");
130         }
131         #endregion
132
133         /// <summary>
134         /// Gets the negotiated audio info.
135         /// </summary>
136         public ScreenMirroringAudioInfo AudioInfo { get; }
137
138         /// <summary>
139         /// Gets the negotiated video info.
140         /// </summary>
141         public ScreenMirroringVideoInfo VideoInfo { get; }
142
143         private bool IsConnected
144         {
145             get
146             {
147                 return _state.IsOneOf(ScreenMirroringState.Connected, ScreenMirroringState.Playing,
148                     ScreenMirroringState.Paused);
149             }
150         }
151
152         internal void ThrowIfNotConnected()
153         {
154             ThrowIfDisposed();
155
156             if (IsConnected == false)
157             {
158                 throw new InvalidOperationException("ScreenMirroring is not connected.");
159             }
160         }
161
162         /// <summary>
163         /// Prepares the screen mirroring with the specified display.
164         /// </summary>
165         /// <remarks>
166         /// The state must be <see cref="ScreenMirroringState.Idle"/>.\n
167         /// \n
168         /// All supported resolutions will be candidates.
169         /// </remarks>
170         /// <param name="display">The display where the mirroring will be played on.</param>
171         /// <exception cref="ArgumentException">
172         ///    <paramref name="display"/> has already been assigned to another.
173         /// </exception>
174         /// <exception cref="ArgumentNullException"><paramref name="display"/> is null.</exception>
175         /// <exception cref="InvalidOperationException">
176         ///     The current state is not in the valid.\n
177         ///     -or-\n
178         ///     An internal error occurs.
179         /// </exception>
180         /// <exception cref="ObjectDisposedException">The <see cref="ScreenMirroring"/> has already been disposed.</exception>
181         public void Prepare(Display display)
182         {
183             PrepareCore(display, (ScreenMirroringResolutions)0);
184         }
185
186         /// <summary>
187         /// Prepares the screen mirroring with the specified display and resolutions.
188         /// </summary>
189         /// <remarks>
190         /// The state must be <see cref="ScreenMirroringState.Idle"/>.
191         /// </remarks>
192         /// <param name="display">The display where the mirroring will be played on.</param>
193         /// <param name="resolutions">The desired resolutions.</param>
194         /// <exception cref="ArgumentException">
195         ///    <paramref name="resolutions"/> contain invalid flags.\n
196         ///    -or-\n
197         ///    <paramref name="display"/> has already been assigned to another.
198         /// </exception>
199         /// <exception cref="ArgumentNullException"><paramref name="display"/> is null.</exception>
200         /// <exception cref="InvalidOperationException">
201         ///     The current state is not in the valid.\n
202         ///     -or-\n
203         ///     An internal error occurs.
204         /// </exception>
205         /// <exception cref="ObjectDisposedException">The <see cref="ScreenMirroring"/> has already been disposed.</exception>
206         public void Prepare(Display display, ScreenMirroringResolutions resolutions)
207         {
208             ValidationUtil.ValidateFlagsEnum(resolutions, (ScreenMirroringResolutions)((1 << 7) - 1), nameof(resolutions));
209
210             PrepareCore(display, resolutions);
211         }
212
213         private void PrepareCore(Display display, ScreenMirroringResolutions resolutions)
214         {
215             ValidateState(ScreenMirroringState.Idle);
216
217             Native.SetResolution(Handle, resolutions).ThrowIfError("Failed to set resolutions.");
218
219             try
220             {
221                 SetDisplay(display);
222
223                 Native.Prepare(Handle).ThrowIfError("Failed to prepare.");
224             }
225             catch
226             {
227                 DetachDisplay();
228                 throw;
229             }
230         }
231
232         /// <summary>
233         /// Creates the connection and ready for receiving data from a mirroring source.
234         /// </summary>
235         /// <param name="sourceIp">The source ip address to connect.</param>
236         /// <remarks>
237         /// The state must be <see cref="ScreenMirroringState.Prepared"/> state by
238         /// <see cref="Prepare(Display, ScreenMirroringResolutions)"/>.
239         /// </remarks>
240         /// <returns>A task that represents the asynchronous operation.</returns>
241         /// <privilege>http://tizen.org/privilege/internet</privilege>
242         /// <exception cref="ArgumentNullException"><paramref name="sourceIp"/> is null.</exception>
243         /// <exception cref="InvalidOperationException">
244         ///     The current state is not in the valid.\n
245         ///     -or-\n
246         ///     An internal error occurs.
247         /// </exception>
248         /// <exception cref="ArgumentException"><paramref name="sourceIp"/> is a zero-length string, contains only white space.</exception>
249         /// <exception cref="ObjectDisposedException">The <see cref="ScreenMirroring"/> has already been disposed.</exception>
250         /// <exception cref="UnauthorizedAccessException">Caller does not have required permission.</exception>
251         public Task ConnectAsync(string sourceIp)
252         {
253             if (sourceIp == null)
254             {
255                 throw new ArgumentNullException(nameof(sourceIp));
256             }
257
258             if (string.IsNullOrWhiteSpace(sourceIp))
259             {
260                 throw new ArgumentException($"{nameof(sourceIp)} is a zero-length string.", nameof(sourceIp));
261             }
262
263             ValidateState(ScreenMirroringState.Prepared);
264
265             Native.SetIpAndPort(Handle, sourceIp, Port.ToString()).ThrowIfError("Failed to set ip.");
266
267             var tcs = new TaskCompletionSource<bool>();
268
269             Task.Factory.StartNew(() =>
270             {
271                 Native.Connect(Handle).ThrowIfError("Failed to connect");
272                 tcs.SetResult(true);
273             });
274
275             return tcs.Task;
276         }
277
278         /// <summary>
279         /// Starts mirroring from the source.
280         /// </summary>
281         /// <remarks>
282         /// The state must be <see cref="ScreenMirroringState.Connected"/> state by
283         /// <see cref="ConnectAsync(string)"/>.
284         /// </remarks>
285         /// <returns>A task that represents the asynchronous operation.</returns>
286         /// <privilege>http://tizen.org/privilege/internet</privilege>
287         /// <exception cref="InvalidOperationException">
288         ///     The current state is not in the valid.\n
289         ///     -or-\n
290         ///     An internal error occurs.
291         /// </exception>
292         /// <exception cref="ObjectDisposedException">The <see cref="ScreenMirroring"/> has already been disposed.</exception>
293         /// <exception cref="UnauthorizedAccessException">Caller does not have required permission.</exception>
294         public Task StartAsync()
295         {
296             ValidateState(ScreenMirroringState.Connected);
297
298             var tcs = new TaskCompletionSource<bool>();
299
300             Task.Factory.StartNew(() =>
301             {
302                 Native.StartAsync(Handle).ThrowIfError("Failed to start.");
303                 tcs.TrySetResult(true);
304             });
305
306             return tcs.Task;
307         }
308
309         /// <summary>
310         /// Pauses mirroring from the source.
311         /// </summary>
312         /// <remarks>
313         /// The state must be <see cref="ScreenMirroringState.Playing"/> state by
314         /// <see cref="StartAsync"/>.
315         /// </remarks>
316         /// <returns>A task that represents the asynchronous operation.</returns>
317         /// <privilege>http://tizen.org/privilege/internet</privilege>
318         /// <exception cref="InvalidOperationException">
319         ///     The current state is not in the valid.\n
320         ///     -or-\n
321         ///     An internal error occurs.
322         /// </exception>
323         /// <exception cref="ObjectDisposedException">The <see cref="ScreenMirroring"/> has already been disposed.</exception>
324         /// <exception cref="UnauthorizedAccessException">Caller does not have required permission.</exception>
325         public Task PauseAsync()
326         {
327             ValidateState(ScreenMirroringState.Playing);
328
329             var tcs = new TaskCompletionSource<bool>();
330
331             Task.Factory.StartNew(() =>
332             {
333                 Native.PauseAsync(Handle).ThrowIfError("Failed to prepare.");
334                 tcs.TrySetResult(true);
335             });
336
337             return tcs.Task;
338         }
339
340         /// <summary>
341         /// Resumes mirroring from the source.
342         /// </summary>
343         /// <remarks>
344         /// The state must be <see cref="ScreenMirroringState.Paused"/> state by
345         /// <see cref="PauseAsync"/>.
346         /// </remarks>
347         /// <returns>A task that represents the asynchronous operation.</returns>
348         /// <privilege>http://tizen.org/privilege/internet</privilege>
349         /// <exception cref="InvalidOperationException">
350         ///     The current state is not in the valid.\n
351         ///     -or-\n
352         ///     An internal error occurs.
353         /// </exception>
354         /// <exception cref="ObjectDisposedException">The <see cref="ScreenMirroring"/> has already been disposed.</exception>
355         /// <exception cref="UnauthorizedAccessException">Caller does not have required permission.</exception>
356         public Task ResumeAsync()
357         {
358             ValidateState(ScreenMirroringState.Paused);
359
360             var tcs = new TaskCompletionSource<bool>();
361
362             Task.Factory.StartNew(() =>
363             {
364                 Native.ResumeAsync(Handle).ThrowIfError("Failed to resume.");
365                 tcs.TrySetResult(true);
366             });
367
368             return tcs.Task;
369         }
370
371         /// <summary>
372         /// Disconnects from the source.
373         /// </summary>
374         /// <remarks>
375         /// The state must be <see cref="ScreenMirroringState.Connected"/>,
376         /// <see cref="ScreenMirroringState.Playing"/> or <see cref="ScreenMirroringState.Paused"/>.
377         /// </remarks>
378         /// <privilege>http://tizen.org/privilege/internet</privilege>
379         /// <exception cref="InvalidOperationException">
380         ///     The current state is not in the valid.\n
381         ///     -or-\n
382         ///     An internal error occurs.
383         /// </exception>
384         /// <exception cref="ObjectDisposedException">The <see cref="ScreenMirroring"/> has already been disposed.</exception>
385         /// <exception cref="UnauthorizedAccessException">Caller does not have required permission.</exception>
386         public void Disconnect()
387         {
388             ValidateState(ScreenMirroringState.Connected, ScreenMirroringState.Playing,
389                 ScreenMirroringState.Paused);
390
391             Native.Disconnect(Handle).ThrowIfError("Failed to disconnect.");
392         }
393
394         /// <summary>
395         /// Unprepares the screen mirroring.
396         /// </summary>
397         /// <remarks>
398         /// The state must be <see cref="ScreenMirroringState.Prepared"/>,
399         /// or <see cref="ScreenMirroringState.Disconnected"/>.
400         /// </remarks>
401         /// <exception cref="InvalidOperationException">
402         ///     The current state is not in the valid.\n
403         ///     -or-\n
404         ///     An internal error occurs.
405         /// </exception>
406         /// <exception cref="ObjectDisposedException">The <see cref="ScreenMirroring"/> has already been disposed.</exception>
407         public void Unprepare()
408         {
409             ValidateState(ScreenMirroringState.Prepared, ScreenMirroringState.Disconnected);
410
411             Native.Unprepare(Handle).ThrowIfError("Failed to reset.");
412
413             DetachDisplay();
414         }
415
416         private void ThrowIfDisposed()
417         {
418             if (_disposed)
419             {
420                 throw new ObjectDisposedException(nameof(ScreenMirroring));
421             }
422         }
423
424         /// <summary>
425         /// Releases all resource used by the <see cref="ScreenMirroring"/> object.
426         /// </summary>
427         /// <remarks>
428         /// Call <see cref="Dispose()"/> when you are finished using the <see cref="ScreenMirroring"/>.
429         /// The <see cref="Dispose()"/> method leaves the <see cref="ScreenMirroring"/> in an unusable
430         /// state. After calling <see cref="Dispose"()/>, you must release all references to the
431         /// <see cref="ScreenMirroring"/> so the garbage collector can reclaim the memory that the
432         /// <see cref="ScreenMirroring"/> was occupying.
433         /// </remarks>
434         public void Dispose()
435         {
436             Dispose(true);
437             GC.SuppressFinalize(this);
438         }
439
440         /// <summary>
441         /// Releases the resources used by the ScreenMirroring.
442         /// </summary>
443         /// <param name="disposing">
444         /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
445         /// </param>
446         protected virtual void Dispose(bool disposing)
447         {
448             if (!_disposed)
449             {
450                 DetachDisplay();
451
452                 if (_handle != IntPtr.Zero)
453                 {
454                     Native.Destroy(_handle);
455                     _handle = IntPtr.Zero;
456                 }
457
458                 _disposed = true;
459             }
460         }
461
462         private Native.StateChangedCallback _stateChangedCallback;
463
464         private void RegisterStateChangedEvent()
465         {
466             _stateChangedCallback = (error, state, _) =>
467             {
468                 var prevState = _state.Value;
469
470                 _state.Value = state;
471
472                 if (prevState != state)
473                 {
474                     StateChanged?.Invoke(this, new ScreenMirroringStateChangedEventArgs(state));
475
476                 }
477
478                 if (error != ScreenMirroringErrorCode.None)
479                 {
480                     ErrorOccurred?.Invoke(this, new ScreenMirroringErrorOccurredEventArgs(
481                         ScreenMirroringError.InvalidOperation));
482                 }
483             };
484
485             Native.SetStateChangedCb(Handle, _stateChangedCallback).
486                 ThrowIfError("Failed to initialize StateChanged event.");
487         }
488
489         private void ValidateState(params ScreenMirroringState[] required)
490         {
491             Debug.Assert(required.Length > 0);
492
493             if (_disposed)
494             {
495                 throw new ObjectDisposedException(nameof(ScreenMirroring));
496             }
497
498             var curState = _state.Value;
499             if (!required.Contains(curState))
500             {
501                 throw new InvalidOperationException($"The screen mirroring is not in a valid state. " +
502                     $"Current State : { curState }, Valid State : { string.Join(", ", required) }.");
503             }
504         }
505
506     }
507
508     internal class AtomicState
509     {
510         private int _value;
511
512         public AtomicState()
513         {
514             _value = (int)ScreenMirroringState.Idle;
515         }
516
517         public ScreenMirroringState Value
518         {
519             get
520             {
521                 return (ScreenMirroringState)Interlocked.CompareExchange(ref _value, 0, 0);
522             }
523             set
524             {
525                 Interlocked.Exchange(ref _value, (int)value);
526             }
527         }
528
529         public bool IsOneOf(params ScreenMirroringState[] states)
530         {
531             return states.Contains(Value);
532         }
533     }
534 }