Implement the Remote Playback API.
[WebKit-https.git] / Source / WebCore / Modules / mediasession / WebMediaSessionManager.cpp
1 /*
2  * Copyright (C) 2015 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 "WebMediaSessionManager.h"
28
29 #if ENABLE(WIRELESS_PLAYBACK_TARGET) && !PLATFORM(IOS_FAMILY)
30
31 #include "FloatRect.h"
32 #include "Logging.h"
33 #include "MediaPlaybackTargetPickerMock.h"
34 #include "WebMediaSessionManagerClient.h"
35 #include <wtf/text/StringBuilder.h>
36
37 namespace WebCore {
38
39 static const Seconds taskDelayInterval { 100_ms };
40
41 struct ClientState {
42     WTF_MAKE_STRUCT_FAST_ALLOCATED;
43
44     explicit ClientState(WebMediaSessionManagerClient& client, uint64_t contextId)
45         : client(client)
46         , contextId(contextId)
47     {
48     }
49
50     bool operator == (ClientState const& other) const
51     {
52         return contextId == other.contextId && &client == &other.client;
53     }
54
55     WebMediaSessionManagerClient& client;
56     uint64_t contextId { 0 };
57     WebCore::MediaProducer::MediaStateFlags flags { WebCore::MediaProducer::IsNotPlaying };
58     bool requestedPicker { false };
59     bool previouslyRequestedPicker { false };
60     bool configurationRequired { true };
61     bool playedToEnd { false };
62 };
63
64 static bool flagsAreSet(MediaProducer::MediaStateFlags value, unsigned flags)
65 {
66     return value & flags;
67 }
68
69 #if !LOG_DISABLED
70 static String mediaProducerStateString(MediaProducer::MediaStateFlags flags)
71 {
72     StringBuilder string;
73     if (flags & MediaProducer::IsPlayingAudio)
74         string.append("IsPlayingAudio + ");
75     if (flags & MediaProducer::IsPlayingVideo)
76         string.append("IsPlayingVideo + ");
77     if (flags & MediaProducer::IsPlayingToExternalDevice)
78         string.append("IsPlayingToExternalDevice + ");
79     if (flags & MediaProducer::HasPlaybackTargetAvailabilityListener)
80         string.append("HasPlaybackTargetAvailabilityListener + ");
81     if (flags & MediaProducer::RequiresPlaybackTargetMonitoring)
82         string.append("RequiresPlaybackTargetMonitoring + ");
83     if (flags & MediaProducer::ExternalDeviceAutoPlayCandidate)
84         string.append("ExternalDeviceAutoPlayCandidate + ");
85     if (flags & MediaProducer::DidPlayToEnd)
86         string.append("DidPlayToEnd + ");
87     if (flags & MediaProducer::HasAudioOrVideo)
88         string.append("HasAudioOrVideo + ");
89     if (string.isEmpty())
90         string.append("IsNotPlaying");
91     else
92         string.resize(string.length() - 2);
93
94     return string.toString();
95 }
96 #endif
97
98 void WebMediaSessionManager::setMockMediaPlaybackTargetPickerEnabled(bool enabled)
99 {
100     LOG(Media, "WebMediaSessionManager::setMockMediaPlaybackTargetPickerEnabled - enabled = %i", (int)enabled);
101
102     if (m_mockPickerEnabled == enabled)
103         return;
104
105     m_mockPickerEnabled = enabled;
106 }
107
108 void WebMediaSessionManager::setMockMediaPlaybackTargetPickerState(const String& name, MediaPlaybackTargetContext::State state)
109 {
110     LOG(Media, "WebMediaSessionManager::setMockMediaPlaybackTargetPickerState - name = %s, state = %i", name.utf8().data(), (int)state);
111
112     mockPicker().setState(name, state);
113 }
114
115 void WebMediaSessionManager::mockMediaPlaybackTargetPickerDismissPopup()
116 {
117     LOG(Media, "WebMediaSessionManager::mockMediaPlaybackTargetPickerDismissPopup");
118
119     mockPicker().dismissPopup();
120 }
121
122 MediaPlaybackTargetPickerMock& WebMediaSessionManager::mockPicker()
123 {
124     if (!m_pickerOverride)
125         m_pickerOverride = makeUnique<MediaPlaybackTargetPickerMock>(*this);
126
127     return *m_pickerOverride.get();
128 }
129
130 WebCore::MediaPlaybackTargetPicker& WebMediaSessionManager::targetPicker()
131 {
132     if (m_mockPickerEnabled)
133         return mockPicker();
134
135     return platformPicker();
136 }
137
138 WebMediaSessionManager::WebMediaSessionManager()
139     : m_taskTimer(RunLoop::current(), this, &WebMediaSessionManager::taskTimerFired)
140     , m_watchdogTimer(RunLoop::current(), this, &WebMediaSessionManager::watchdogTimerFired)
141 {
142 }
143
144 WebMediaSessionManager::~WebMediaSessionManager() = default;
145
146 uint64_t WebMediaSessionManager::addPlaybackTargetPickerClient(WebMediaSessionManagerClient& client, uint64_t contextId)
147 {
148     size_t index = find(&client, contextId);
149     ASSERT(index == notFound);
150     if (index != notFound)
151         return 0;
152
153     LOG(Media, "WebMediaSessionManager::addPlaybackTargetPickerClient(%p + %llu)", &client, contextId);
154
155     m_clientState.append(makeUnique<ClientState>(client, contextId));
156
157     if (m_externalOutputDeviceAvailable || m_playbackTarget)
158         scheduleDelayedTask(InitialConfigurationTask | TargetClientsConfigurationTask);
159
160     return contextId;
161 }
162
163 void WebMediaSessionManager::removePlaybackTargetPickerClient(WebMediaSessionManagerClient& client, uint64_t contextId)
164 {
165     size_t index = find(&client, contextId);
166     ASSERT(index != notFound);
167     if (index == notFound)
168         return;
169
170     LOG(Media, "WebMediaSessionManager::removePlaybackTargetPickerClient(%p + %llu)", &client, contextId);
171
172     m_clientState.remove(index);
173     scheduleDelayedTask(TargetMonitoringConfigurationTask | TargetClientsConfigurationTask);
174 }
175
176 void WebMediaSessionManager::removeAllPlaybackTargetPickerClients(WebMediaSessionManagerClient& client)
177 {
178     if (m_clientState.isEmpty())
179         return;
180
181     LOG(Media, "WebMediaSessionManager::removeAllPlaybackTargetPickerClients(%p)", &client);
182
183     for (size_t i = m_clientState.size(); i > 0; --i) {
184         if (&m_clientState[i - 1]->client == &client)
185             m_clientState.remove(i - 1);
186     }
187     scheduleDelayedTask(TargetMonitoringConfigurationTask | TargetClientsConfigurationTask);
188 }
189
190 void WebMediaSessionManager::showPlaybackTargetPicker(WebMediaSessionManagerClient& client, uint64_t contextId, const IntRect& rect, bool, bool useDarkAppearance)
191 {
192     size_t index = find(&client, contextId);
193     ASSERT(index != notFound);
194     if (index == notFound)
195         return;
196
197     auto& clientRequestingPicker = m_clientState[index];
198     for (auto& state : m_clientState) {
199         state->requestedPicker = state == clientRequestingPicker;
200         state->previouslyRequestedPicker = state == clientRequestingPicker;
201     }
202
203     bool hasActiveRoute = flagsAreSet(m_clientState[index]->flags, MediaProducer::IsPlayingToExternalDevice);
204     LOG(Media, "WebMediaSessionManager::showPlaybackTargetPicker(%p + %llu) - hasActiveRoute = %i", &client, contextId, (int)hasActiveRoute);
205     targetPicker().showPlaybackTargetPicker(FloatRect(rect), hasActiveRoute, useDarkAppearance);
206 }
207
208 void WebMediaSessionManager::clientStateDidChange(WebMediaSessionManagerClient& client, uint64_t contextId, MediaProducer::MediaStateFlags newFlags)
209 {
210     size_t index = find(&client, contextId);
211     ASSERT(index != notFound);
212     if (index == notFound)
213         return;
214
215     auto& changedClientState = m_clientState[index];
216     MediaProducer::MediaStateFlags oldFlags = changedClientState->flags;
217     if (newFlags == oldFlags)
218         return;
219
220     LOG(Media, "WebMediaSessionManager::clientStateDidChange(%p + %llu) - new flags = %s, old flags = %s", &client, contextId, mediaProducerStateString(newFlags).utf8().data(), mediaProducerStateString(oldFlags).utf8().data());
221
222     changedClientState->flags = newFlags;
223
224     MediaProducer::MediaStateFlags updateConfigurationFlags = MediaProducer::RequiresPlaybackTargetMonitoring | MediaProducer::HasPlaybackTargetAvailabilityListener | MediaProducer::HasAudioOrVideo;
225     if ((oldFlags & updateConfigurationFlags) != (newFlags & updateConfigurationFlags))
226         scheduleDelayedTask(TargetMonitoringConfigurationTask);
227
228     MediaProducer::MediaStateFlags playingToTargetFlags = MediaProducer::IsPlayingToExternalDevice | MediaProducer::IsPlayingVideo;
229     if ((oldFlags & playingToTargetFlags) != (newFlags & playingToTargetFlags)) {
230         if (flagsAreSet(oldFlags, MediaProducer::IsPlayingVideo) && !flagsAreSet(newFlags, MediaProducer::IsPlayingVideo) && flagsAreSet(newFlags, MediaProducer::DidPlayToEnd))
231             changedClientState->playedToEnd = true;
232         scheduleDelayedTask(WatchdogTimerConfigurationTask);
233     }
234
235     if (!m_playbackTarget || !m_playbackTarget->hasActiveRoute() || !flagsAreSet(newFlags, MediaProducer::ExternalDeviceAutoPlayCandidate))
236         return;
237
238     // Do not interrupt another element already playing to a device.
239     for (auto& state : m_clientState) {
240         if (state == changedClientState)
241             continue;
242
243         if (flagsAreSet(state->flags, MediaProducer::IsPlayingToExternalDevice) && flagsAreSet(state->flags, MediaProducer::IsPlayingVideo))
244             return;
245     }
246
247     // Do not begin playing to the device unless playback has just started.
248     if (!flagsAreSet(newFlags, MediaProducer::IsPlayingVideo) || flagsAreSet(oldFlags, MediaProducer::IsPlayingVideo))
249         return;
250
251     for (auto& state : m_clientState) {
252         if (state == changedClientState)
253             continue;
254         state->client.setShouldPlayToPlaybackTarget(state->contextId, false);
255     }
256
257     changedClientState->client.setShouldPlayToPlaybackTarget(changedClientState->contextId, true);
258
259     if (index && m_clientState.size() > 1)
260         std::swap(m_clientState.at(index), m_clientState.at(0));
261 }
262
263 void WebMediaSessionManager::setPlaybackTarget(Ref<MediaPlaybackTarget>&& target)
264 {
265     m_playbackTarget = WTFMove(target);
266     m_targetChanged = true;
267     scheduleDelayedTask(TargetClientsConfigurationTask);
268 }
269
270 void WebMediaSessionManager::externalOutputDeviceAvailableDidChange(bool available)
271 {
272     LOG(Media, "WebMediaSessionManager::externalOutputDeviceAvailableDidChange - clients = %zu, available = %i", m_clientState.size(), (int)available);
273
274     m_externalOutputDeviceAvailable = available;
275     for (auto& state : m_clientState)
276         state->client.externalOutputDeviceAvailableDidChange(state->contextId, available);
277 }
278
279 void WebMediaSessionManager::playbackTargetPickerWasDismissed()
280 {
281     m_playbackTargetPickerDismissed = true;
282     scheduleDelayedTask(TargetClientsConfigurationTask);
283 }
284
285 void WebMediaSessionManager::configureNewClients()
286 {
287     for (auto& state : m_clientState) {
288         if (!state->configurationRequired)
289             continue;
290
291         state->configurationRequired = false;
292         if (m_externalOutputDeviceAvailable)
293             state->client.externalOutputDeviceAvailableDidChange(state->contextId, true);
294
295         if (m_playbackTarget)
296             state->client.setPlaybackTarget(state->contextId, *m_playbackTarget.copyRef());
297     }
298 }
299
300 void WebMediaSessionManager::configurePlaybackTargetClients()
301 {
302     if (m_clientState.isEmpty())
303         return;
304
305     size_t indexOfClientThatRequestedPicker = notFound;
306     size_t indexOfLastClientToRequestPicker = notFound;
307     size_t indexOfClientWillPlayToTarget = notFound;
308     bool haveActiveRoute = m_playbackTarget && m_playbackTarget->hasActiveRoute();
309
310     for (size_t i = 0; i < m_clientState.size(); ++i) {
311         auto& state = m_clientState[i];
312
313         LOG(Media, "WebMediaSessionManager::configurePlaybackTargetClients %zu - client (%p + %llu) requestedPicker = %i, flags = %s", i, &state->client, state->contextId, state->requestedPicker, mediaProducerStateString(state->flags).utf8().data());
314
315         if ((m_targetChanged || m_playbackTargetPickerDismissed) && state->requestedPicker)
316             indexOfClientThatRequestedPicker = i;
317
318         if (indexOfClientWillPlayToTarget == notFound && flagsAreSet(state->flags, MediaProducer::IsPlayingToExternalDevice))
319             indexOfClientWillPlayToTarget = i;
320
321         if (indexOfClientWillPlayToTarget == notFound && haveActiveRoute && state->previouslyRequestedPicker)
322             indexOfLastClientToRequestPicker = i;
323     }
324
325     if (indexOfClientThatRequestedPicker != notFound)
326         indexOfClientWillPlayToTarget = indexOfClientThatRequestedPicker;
327     if (indexOfClientWillPlayToTarget == notFound && indexOfLastClientToRequestPicker != notFound)
328         indexOfClientWillPlayToTarget = indexOfLastClientToRequestPicker;
329     if (indexOfClientWillPlayToTarget == notFound && haveActiveRoute && flagsAreSet(m_clientState[0]->flags, MediaProducer::ExternalDeviceAutoPlayCandidate) && !flagsAreSet(m_clientState[0]->flags, MediaProducer::IsPlayingVideo))
330         indexOfClientWillPlayToTarget = 0;
331
332     LOG(Media, "WebMediaSessionManager::configurePlaybackTargetClients - indexOfClientWillPlayToTarget = %zu", indexOfClientWillPlayToTarget);
333
334     for (size_t i = 0; i < m_clientState.size(); ++i) {
335         auto& state = m_clientState[i];
336
337         if (m_playbackTarget)
338             state->client.setPlaybackTarget(state->contextId, *m_playbackTarget.copyRef());
339
340         if (i != indexOfClientWillPlayToTarget || !haveActiveRoute)
341             state->client.setShouldPlayToPlaybackTarget(state->contextId, false);
342
343         if (state->requestedPicker && m_playbackTargetPickerDismissed)
344             state->client.playbackTargetPickerWasDismissed(state->contextId);
345
346         state->configurationRequired = false;
347         if ((m_targetChanged || m_playbackTargetPickerDismissed))
348             state->requestedPicker = false;
349     }
350
351     if (haveActiveRoute && indexOfClientWillPlayToTarget != notFound) {
352         auto& state = m_clientState[indexOfClientWillPlayToTarget];
353         if (!flagsAreSet(state->flags, MediaProducer::IsPlayingToExternalDevice))
354             state->client.setShouldPlayToPlaybackTarget(state->contextId, true);
355     }
356
357     m_targetChanged = false;
358     configureWatchdogTimer();
359 }
360
361 void WebMediaSessionManager::configurePlaybackTargetMonitoring()
362 {
363     bool monitoringRequired = false;
364     bool hasAvailabilityListener = false;
365     bool haveClientWithMedia = false;
366     for (auto& state : m_clientState) {
367         if (state->flags & MediaProducer::RequiresPlaybackTargetMonitoring) {
368             monitoringRequired = true;
369             break;
370         }
371         if (state->flags & MediaProducer::HasPlaybackTargetAvailabilityListener)
372             hasAvailabilityListener = true;
373         if (state->flags & MediaProducer::HasAudioOrVideo)
374             haveClientWithMedia = true;
375     }
376
377     LOG(Media, "WebMediaSessionManager::configurePlaybackTargetMonitoring - monitoringRequired = %i", static_cast<int>(monitoringRequired || (hasAvailabilityListener && haveClientWithMedia)));
378
379     if (monitoringRequired || (hasAvailabilityListener && haveClientWithMedia))
380         targetPicker().startingMonitoringPlaybackTargets();
381     else
382         targetPicker().stopMonitoringPlaybackTargets();
383 }
384
385 #if !LOG_DISABLED
386 String WebMediaSessionManager::toString(ConfigurationTasks tasks)
387 {
388     StringBuilder string;
389     if (tasks & InitialConfigurationTask)
390         string.append("InitialConfigurationTask + ");
391     if (tasks & TargetClientsConfigurationTask)
392         string.append("TargetClientsConfigurationTask + ");
393     if (tasks & TargetMonitoringConfigurationTask)
394         string.append("TargetMonitoringConfigurationTask + ");
395     if (tasks & WatchdogTimerConfigurationTask)
396         string.append("WatchdogTimerConfigurationTask + ");
397     if (string.isEmpty())
398         string.append("NoTask");
399     else
400         string.resize(string.length() - 2);
401     
402     return string.toString();
403 }
404 #endif
405
406 void WebMediaSessionManager::scheduleDelayedTask(ConfigurationTasks tasks)
407 {
408     LOG(Media, "WebMediaSessionManager::scheduleDelayedTask - %s", toString(tasks).utf8().data());
409
410     m_taskFlags |= tasks;
411     m_taskTimer.startOneShot(taskDelayInterval);
412 }
413
414 void WebMediaSessionManager::taskTimerFired()
415 {
416     LOG(Media, "WebMediaSessionManager::taskTimerFired - tasks = %s", toString(m_taskFlags).utf8().data());
417
418     if (m_taskFlags & InitialConfigurationTask)
419         configureNewClients();
420     if (m_taskFlags & TargetClientsConfigurationTask)
421         configurePlaybackTargetClients();
422     if (m_taskFlags & TargetMonitoringConfigurationTask)
423         configurePlaybackTargetMonitoring();
424     if (m_taskFlags & WatchdogTimerConfigurationTask)
425         configureWatchdogTimer();
426
427     m_taskFlags = NoTask;
428 }
429
430 size_t WebMediaSessionManager::find(WebMediaSessionManagerClient* client, uint64_t contextId)
431 {
432     for (size_t i = 0; i < m_clientState.size(); ++i) {
433         if (m_clientState[i]->contextId == contextId && &m_clientState[i]->client == client)
434             return i;
435     }
436
437     return notFound;
438 }
439
440 void WebMediaSessionManager::configureWatchdogTimer()
441 {
442     static const Seconds watchdogTimerIntervalAfterPausing { 1_h };
443     static const Seconds watchdogTimerIntervalAfterPlayingToEnd { 8_min };
444
445     if (!m_playbackTarget || !m_playbackTarget->hasActiveRoute()) {
446         m_watchdogTimer.stop();
447         return;
448     }
449
450     bool stopTimer = false;
451     bool didPlayToEnd = false;
452     for (auto& state : m_clientState) {
453         if (flagsAreSet(state->flags, MediaProducer::IsPlayingToExternalDevice) && flagsAreSet(state->flags, MediaProducer::IsPlayingVideo))
454             stopTimer = true;
455         if (state->playedToEnd)
456             didPlayToEnd = true;
457         state->playedToEnd = false;
458     }
459
460     if (stopTimer) {
461         m_currentWatchdogInterval = { };
462         m_watchdogTimer.stop();
463         LOG(Media, "WebMediaSessionManager::configureWatchdogTimer - timer stopped");
464     } else {
465         Seconds interval = didPlayToEnd ? watchdogTimerIntervalAfterPlayingToEnd : watchdogTimerIntervalAfterPausing;
466         if (interval != m_currentWatchdogInterval || !m_watchdogTimer.isActive()) {
467             m_watchdogTimer.startOneShot(interval);
468             LOG(Media, "WebMediaSessionManager::configureWatchdogTimer - timer scheduled for %.0f seconds", interval.value());
469         }
470         m_currentWatchdogInterval = interval;
471     }
472 }
473
474 void WebMediaSessionManager::watchdogTimerFired()
475 {
476     LOG(Media, "WebMediaSessionManager::watchdogTimerFired");
477     if (!m_playbackTarget)
478         return;
479
480     targetPicker().invalidatePlaybackTargets();
481 }
482
483 } // namespace WebCore
484
485 #endif // ENABLE(WIRELESS_PLAYBACK_TARGET) && !PLATFORM(IOS_FAMILY)