Implement the Remote Playback API.
[WebKit-https.git] / Source / WebCore / Modules / remoteplayback / RemotePlayback.cpp
1 /*
2  * Copyright (C) 2019 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #include "config.h"
27 #include "RemotePlayback.h"
28
29 #if ENABLE(WIRELESS_PLAYBACK_TARGET)
30
31 #include "Event.h"
32 #include "EventNames.h"
33 #include "HTMLMediaElement.h"
34 #include "JSDOMPromiseDeferred.h"
35 #include "Logging.h"
36 #include "MediaElementSession.h"
37 #include "MediaPlaybackTarget.h"
38 #include "RemotePlaybackAvailabilityCallback.h"
39 #include <wtf/IsoMallocInlines.h>
40
41 namespace WebCore {
42
43 WTF_MAKE_ISO_ALLOCATED_IMPL(RemotePlayback);
44
45 Ref<RemotePlayback> RemotePlayback::create(HTMLMediaElement& element)
46 {
47     return adoptRef(*new RemotePlayback(element));
48 }
49
50 RemotePlayback::RemotePlayback(HTMLMediaElement& element)
51     : WebCore::ActiveDOMObject(element.scriptExecutionContext())
52     , m_mediaElement(makeWeakPtr(element))
53     , m_eventQueue(MainThreadGenericEventQueue::create(*this))
54 {
55     suspendIfNeeded();
56 }
57
58 RemotePlayback::~RemotePlayback()
59 {
60 }
61
62 void RemotePlayback::watchAvailability(Ref<RemotePlaybackAvailabilityCallback>&& callback, Ref<DeferredPromise>&& promise)
63 {
64     // 6.2.1.3 Getting the remote playback devices availability information
65     // https://w3c.github.io/remote-playback/#monitoring-the-list-of-available-remote-playback-devices
66     // W3C Editor's Draft 15 July 2016
67
68     // 1. Let promise be a new promise->
69     // 2. Return promise, and run the following steps below:
70     
71     m_taskQueue.enqueueTask([this, callback = WTFMove(callback), promise = WTFMove(promise)] () mutable {
72         // 3. If the disableRemotePlayback attribute is present for the media element, reject the promise with
73         //    InvalidStateError and abort all the remaining steps.
74         if (!m_mediaElement
75             || m_mediaElement->hasAttributeWithoutSynchronization(HTMLNames::webkitwirelessvideoplaybackdisabledAttr)
76             || m_mediaElement->hasAttributeWithoutSynchronization(HTMLNames::disableremoteplaybackAttr)) {
77             WTFLogAlways("RemotePlayback::watchAvailability()::task - promise rejected");
78             promise->reject(InvalidStateError);
79             return;
80         }
81
82         // 4. If the user agent is unable to monitor the list of available remote playback devices for the entire
83         //    lifetime of the browsing context (for instance, because the user has disabled this feature), then run
84         //    the following steps in parallel:
85         // 5. If the user agent is unable to continuously monitor the list of available remote playback devices but
86         //    can do it for a short period of time when initiating remote playback, then:
87         // NOTE: Unimplemented; all current ports can support continuous device monitoring
88
89         // 6. Let callbackId be a number unique to the media element that will identify the callback.
90         int32_t callbackId = ++m_nextId;
91
92         // 7. Create a tuple (callbackId, callback) and add it to the set of availability callbacks for this media element.
93         ASSERT(!m_callbackMap.contains(callbackId));
94         m_callbackMap.add(callbackId, WTFMove(callback));
95
96         // 8. Fulfill promise with the callbackId and run the following steps in parallel:
97         promise->whenSettled([this, protectedThis = makeRefPtr(this), callbackId] {
98             // 8.1 Queue a task to invoke the callback with the current availability for the media element.
99             m_taskQueue.enqueueTask([this, callbackId] {
100                 auto foundCallback = m_callbackMap.find(callbackId);
101                 if (foundCallback == m_callbackMap.end())
102                     return;
103
104                 if (updateAvailability() == UpdateResults::Unchanged)
105                     foundCallback->value->handleEvent(m_available);
106             });
107
108             // 8.2 Run the algorithm to monitor the list of available remote playback devices.
109             m_mediaElement->remoteHasAvailabilityCallbacksChanged();
110         });
111         promise->resolve<IDLLong>(callbackId);
112     });
113 }
114
115 void RemotePlayback::cancelWatchAvailability(Optional<int32_t> id, Ref<DeferredPromise>&& promise)
116 {
117     // 6.2.1.5 Stop observing remote playback devices availability
118     // https://w3c.github.io/remote-playback/#stop-observing-remote-playback-devices-availability
119     // W3C Editor's Draft 15 July 2016
120
121     // 1. Let promise be a new promise->
122     // 2. Return promise, and run the following steps below:
123
124     m_taskQueue.enqueueTask([this, id = WTFMove(id), promise = WTFMove(promise)] {
125         // 3. If the disableRemotePlayback attribute is present for the media element, reject promise with
126         //    InvalidStateError and abort all the remaining steps.
127         if (!m_mediaElement
128             || m_mediaElement->hasAttributeWithoutSynchronization(HTMLNames::webkitwirelessvideoplaybackdisabledAttr)
129             || m_mediaElement->hasAttributeWithoutSynchronization(HTMLNames::disableremoteplaybackAttr)) {
130             promise->reject(InvalidStateError);
131             return;
132         }
133
134         // 4. If the parameter id is undefined, clear the set of availability callbacks.
135         if (!id)
136             m_callbackMap.clear();
137         else {
138             // 5. Otherwise, if id matches the callbackId for any entry in the set of availability callbacks,
139             //    remove the entry from the set.
140             if (auto it = m_callbackMap.find(id.value()) != m_callbackMap.end())
141                 m_callbackMap.remove(it);
142             // 6. Otherwise, reject promise with NotFoundError and abort all the remaining steps.
143             else {
144                 promise->reject(NotFoundError);
145                 return;
146             }
147         }
148         // 7. If the set of availability callbacks is now empty and there is no pending request to initiate remote
149         //    playback, cancel any pending task to monitor the list of available remote playback devices for power
150         //    saving purposes.
151         m_mediaElement->remoteHasAvailabilityCallbacksChanged();
152
153         // 8. Fulfill promise.
154         promise->resolve();
155     });
156 }
157
158 void RemotePlayback::prompt(Ref<DeferredPromise>&& promise)
159 {
160     // 6.2.2 Prompt user for changing remote playback statee
161     // https://w3c.github.io/remote-playback/#stop-observing-remote-playback-devices-availability
162     // W3C Editor's Draft 15 July 2016
163
164     // 1. Let promise be a new promise->
165     // 2. Return promise, and run the following steps below:
166
167     m_taskQueue.enqueueTask([this, promise = WTFMove(promise), processingUserGesture = UserGestureIndicator::processingUserGesture()] () mutable {
168         // 3. If the disableRemotePlayback attribute is present for the media element, reject the promise with
169         //    InvalidStateError and abort all the remaining steps.
170         if (!m_mediaElement
171             || m_mediaElement->hasAttributeWithoutSynchronization(HTMLNames::webkitwirelessvideoplaybackdisabledAttr)
172             || m_mediaElement->hasAttributeWithoutSynchronization(HTMLNames::disableremoteplaybackAttr)) {
173             promise->reject(InvalidStateError);
174             return;
175         }
176
177         // 4. If there is already an unsettled promise from a previous call to prompt for the same media element
178         //     or even for the same browsing context, the user agent may reject promise with an OperationError
179         //     exception and abort all remaining steps.
180         // NOTE: consider implementing
181
182         // 5. OPTIONALLY, if the user agent knows a priori that showing the UI for this particular media element
183         //    is not feasible, reject promise with a NotSupportedError and abort all remaining steps.
184 #if !PLATFORM(IOS)
185         if (m_mediaElement->readyState() < HTMLMediaElementEnums::HAVE_METADATA) {
186             promise->reject(NotSupportedError);
187             return;
188         }
189 #endif
190
191         // 6. If the algorithm isn't allowed to show a popup, reject promise with an InvalidAccessError exception
192         //    and abort these steps.
193         if (!processingUserGesture) {
194             promise->reject(InvalidAccessError);
195             return;
196         }
197
198         // 7. If the user agent needs to show the list of available remote playback devices and is not monitoring
199         //    the list of available remote playback devices, run the steps to monitor the list of available remote
200         //    playback devices in parallel.
201         // NOTE: Monitoring enabled by adding to m_promptPromises and calling remoteHasAvailabilityCallbacksChanged().
202         //       Meanwhile, just update availability for step 9.
203         updateAvailability();
204
205         // 8. If the list of available remote playback devices is empty and will remain so before the request for
206         //    user permission is completed, reject promise with a NotFoundError exception and abort all remaining steps.
207         // NOTE: consider implementing (no network?)
208
209         // 9. If the state is disconnected and availability for the media element is false, reject promise with a
210         //    NotSupportedError exception and abort all remaining steps.
211         if (m_state == State::Disconnected && !m_available) {
212             promise->reject(NotSupportedError);
213             return;
214         }
215
216         m_promptPromises.append(WTFMove(promise));
217         m_mediaElement->remoteHasAvailabilityCallbacksChanged();
218         m_mediaElement->webkitShowPlaybackTargetPicker();
219
220         // NOTE: Steps 10-12 are implemented in the following methods:
221     });
222 }
223
224 void RemotePlayback::shouldPlayToRemoteTargetChanged(bool shouldPlayToRemoteTarget)
225 {
226     // 6.2.2 Prompt user for changing remote playback state [Ctd]
227     // https://w3c.github.io/remote-playback/#prompt-user-for-changing-remote-playback-statee
228     // W3C Editor's Draft 15 July 2016
229
230     LOG(Media, "RemotePlayback::shouldPlayToRemoteTargetChanged(%p), shouldPlay(%d), promise count(%lu)", this, shouldPlayToRemoteTarget, m_promptPromises.size());
231
232     // 10. If the user picked a remote playback device device to initiate remote playback with, the user agent
233     //     must run the following steps:
234     if (shouldPlayToRemoteTarget) {
235         // 10.1 Set the state of the remote object to connecting.
236         // 10.3 Queue a task to fire a simple event with the name connecting at the remote property of the media element.
237         //      The event must not bubble, must not be cancelable, and has no default action.
238         setState(State::Connecting);
239     }
240
241     for (auto& promise : std::exchange(m_promptPromises, { })) {
242         // 10.2 Fulfill promise.
243         // 10.4 Establish a connection with the remote playback device device for the media element.
244         // NOTE: Implemented in establishConnection().
245
246         // 11. Otherwise, if the user chose to disconnect from the remote playback device device, the user agent
247         //     must run the following steps:
248         // 11.1. Fulfill promise.
249         // 11.2. Run the disconnect from remote playback device algorithm for the device.
250         // NOTE: Implemented in disconnect().
251
252         promise->resolve();
253     }
254
255     if (shouldPlayToRemoteTarget)
256         establishConnection();
257     else
258         disconnect();
259
260     m_mediaElement->remoteHasAvailabilityCallbacksChanged();
261 }
262
263 void RemotePlayback::setState(State state)
264 {
265     if (m_state == state)
266         return;
267
268     m_state = state;
269
270     switch (m_state) {
271     case State::Connected:
272         m_eventQueue->enqueueEvent(Event::create(eventNames().connectEvent, Event::CanBubble::No, Event::IsCancelable::No));
273         break;
274     case State::Connecting:
275         m_eventQueue->enqueueEvent(Event::create(eventNames().connectingEvent, Event::CanBubble::No, Event::IsCancelable::No));
276         break;
277     case State::Disconnected:
278         m_eventQueue->enqueueEvent(Event::create(eventNames().disconnectEvent, Event::CanBubble::No, Event::IsCancelable::No));
279         break;
280     }
281 }
282
283 void RemotePlayback::establishConnection()
284 {
285     // 6.2.4 Establishing a connection with a remote playback device
286     // https://w3c.github.io/remote-playback/#establishing-a-connection-with-a-remote-playback-device
287     // W3C Editor's Draft 15 July 2016
288
289     // 1. If the state of remote is not equal to connecting, abort all the remaining steps.
290     if (m_state != State::Connecting)
291         return;
292
293     // 2. Request connection of remote to device. The implementation of this step is specific to the user agent.
294     // NOTE: Handled in MediaPlayer.
295
296     // NOTE: Continued in isPlayingToRemoteTargetChanged()
297 }
298
299 void RemotePlayback::disconnect()
300 {
301     // 6.2.6 Disconnecting from remote playback device
302     // https://w3c.github.io/remote-playback/#dfn-disconnect-from-remote-playback-device
303     // W3C Editor's Draft 15 July 2016
304
305     // 1. If the state of remote is disconnected, abort all remaining steps.
306     if (m_state == State::Disconnected)
307         return;
308
309     // 2. Queue a task to run the following steps:
310     m_taskQueue.enqueueTask([this] {
311         // 2.1 Request disconnection of remote from the device. Implementation is user agent specific.
312         // NOTE: Implemented by MediaPlayer::setWirelessPlaybackTarget()
313         // 2.2 Change the remote's state to disconnected.
314         // 2.3 Fire an event with the name disconnect at remote.
315         setState(State::Disconnected);
316
317         // 2.4 Synchronize the current media element state with the local playback state. Implementation is
318         //     specific to user agent.
319         // NOTE: Handled by the MediaPlayer
320     });
321 }
322
323 RemotePlayback::UpdateResults RemotePlayback::updateAvailability()
324 {
325     bool available = m_mediaElement ? m_mediaElement->mediaSession().hasWirelessPlaybackTargets() : false;
326     if (available == m_available)
327         return UpdateResults::Unchanged;
328
329     availabilityChanged(available);
330     return UpdateResults::Changed;
331 }
332
333 void RemotePlayback::playbackTargetPickerWasDismissed()
334 {
335     // 6.2.2 Prompt user for changing remote playback state [Ctd]
336     // https://w3c.github.io/remote-playback/#stop-observing-remote-playback-devices-availability
337     // W3C Editor's Draft 15 July 2016
338
339     // 12. Otherwise, the user is considered to deny permission to use the device, so reject promise with NotAllowedError
340     // exception and hide the UI shown by the user agent
341     ASSERT(!m_promptPromises.isEmpty());
342
343     for (auto& promise : std::exchange(m_promptPromises, { }))
344         promise->reject(NotAllowedError);
345     m_mediaElement->remoteHasAvailabilityCallbacksChanged();
346 }
347
348 void RemotePlayback::isPlayingToRemoteTargetChanged(bool isPlayingToTarget)
349 {
350     // 6.2.4 Establishing a connection with a remote playback device [Ctd]
351     // https://w3c.github.io/remote-playback/#establishing-a-connection-with-a-remote-playback-device
352     // W3C Editor's Draft 15 July 2016
353
354     // 3. If connection completes successfully, queue a task to run the following steps:
355     if (isPlayingToTarget) {
356         // 3.1. Set the state of remote to connected.
357         // 3.2. Fire a simple event named connect at remote.
358         setState(State::Connected);
359
360         // 3.3 Synchronize the current media element state with the remote playback state. Implementation is
361         //     specific to user agent.
362         // NOTE: Implemented by MediaPlayer.
363         return;
364     }
365
366     // 4. If connection fails, queue a task to run the following steps:
367     // 4.1. Set the remote playback state of remote to disconnected.
368     // 4.2. Fire a simple event named disconnect at remote.
369     setState(State::Disconnected);
370 }
371
372 bool RemotePlayback::hasAvailabilityCallbacks() const
373 {
374     return !m_callbackMap.isEmpty() || !m_promptPromises.isEmpty();
375 }
376
377 void RemotePlayback::availabilityChanged(bool available)
378 {
379     if (available == m_available)
380         return;
381     m_available = available;
382
383     m_taskQueue.enqueueTask([this] {
384         // Protect m_callbackMap against mutation while it's being iterated over.
385         Vector<Ref<RemotePlaybackAvailabilityCallback>> callbacks;
386         callbacks.reserveInitialCapacity(m_callbackMap.size());
387
388         // Can't use copyValuesToVector() here because Ref<> has a deleted assignment operator.
389         for (auto& callback : m_callbackMap.values())
390             callbacks.uncheckedAppend(callback.copyRef());
391         for (auto& callback : callbacks)
392             callback->handleEvent(m_available);
393     });
394 }
395
396 void RemotePlayback::invalidate()
397 {
398     m_mediaElement = nullptr;
399 }
400
401 const char* RemotePlayback::activeDOMObjectName() const
402 {
403     return "RemotePlayback";
404 }
405
406 void RemotePlayback::stop()
407 {
408     m_taskQueue.close();
409     m_eventQueue->close();
410 }
411
412 }
413
414 #endif // ENABLE(WIRELESS_PLAYBACK_TARGET)