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