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. ``AS IS'' AND ANY
14 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
17 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
19 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
20 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
21 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26#include "config.h"
27#include "MediaSession.h"
28
29#if ENABLE(MEDIA_SESSION)
30
31#include "Chrome.h"
32#include "ChromeClient.h"
33#include "Event.h"
34#include "EventNames.h"
35#include "HTMLMediaElement.h"
36#include "MediaSessionManager.h"
37#include "Page.h"
38
39namespace WebCore {
40
41MediaSession::MediaSession(Document& document, Kind kind)
42 : m_document(document)
43 , m_kind(kind)
44{
45 // 4. Media Sessions
46 // 3. If media session's current media session type is "content", then create a new media remote controller for media
47 // session. (Otherwise media session has no media remote controller.)
48 if (m_kind == Kind::Content)
49 m_controls = MediaRemoteControls::create(document, this);
50
51 MediaSessionManager::singleton().addMediaSession(*this);
52}
53
54MediaSession::~MediaSession()
55{
56 MediaSessionManager::singleton().removeMediaSession(*this);
57
58 if (m_controls)
59 m_controls->clearSession();
60}
61
62MediaRemoteControls* MediaSession::controls()
63{
64 return m_controls.get();
65}
66
67void MediaSession::addMediaElement(HTMLMediaElement& element)
68{
69 ASSERT(!m_participatingElements.contains(&element));
70 m_participatingElements.add(&element);
71}
72
73void MediaSession::removeMediaElement(HTMLMediaElement& element)
74{
75 ASSERT(m_participatingElements.contains(&element));
76 m_participatingElements.remove(&element);
77
78 changeActiveMediaElements([&]() {
79 m_activeParticipatingElements.remove(&element);
80 });
81
82 if (m_iteratedActiveParticipatingElements)
83 m_iteratedActiveParticipatingElements->remove(&element);
84}
85
86void MediaSession::changeActiveMediaElements(const WTF::Function<void(void)>& worker)
87{
88 if (Page *page = m_document.page()) {
89 bool hadActiveMediaElements = MediaSessionManager::singleton().hasActiveMediaElements();
90
91 worker();
92
93 bool hasActiveMediaElements = MediaSessionManager::singleton().hasActiveMediaElements();
94 if (hadActiveMediaElements != hasActiveMediaElements)
95 page->chrome().client().hasMediaSessionWithActiveMediaElementsDidChange(hasActiveMediaElements);
96 } else
97 worker();
98}
99
100void MediaSession::addActiveMediaElement(HTMLMediaElement& element)
101{
102 changeActiveMediaElements([&]() {
103 m_activeParticipatingElements.add(&element);
104 });
105}
106
107bool MediaSession::isMediaElementActive(HTMLMediaElement& element)
108{
109 return m_activeParticipatingElements.contains(&element);
110}
111
112bool MediaSession::hasActiveMediaElements() const
113{
114 return !m_activeParticipatingElements.isEmpty();
115}
116
117void MediaSession::setMetadata(const Optional<Metadata>& optionalMetadata)
118{
119 if (!optionalMetadata)
120 m_metadata = { };
121 else {
122 auto& metadata = optionalMetadata.value();
123 m_metadata = { metadata.title, metadata.artist, metadata.album, m_document.completeURL(metadata.artwork) };
124 }
125
126 if (auto* page = m_document.page())
127 page->chrome().client().mediaSessionMetadataDidChange(m_metadata);
128}
129
130void MediaSession::deactivate()
131{
132 // 5.1.2. Object members
133 // When the deactivate() method is invoked, the user agent must run the following steps:
134 // 1. Let media session be the current media session.
135 // 2. Indefinitely pause all of media session’s audio-producing participants.
136 // 3. Set media session's resume list to an empty list.
137 // 4. Set media session's audio-producing participants to an empty list.
138 changeActiveMediaElements([&]() {
139 while (!m_activeParticipatingElements.isEmpty())
140 m_activeParticipatingElements.takeAny()->pause();
141 });
142
143 // 5. Run the media session deactivation algorithm for media session.
144 releaseInternal();
145}
146
147void MediaSession::releaseInternal()
148{
149 // 6.5. Releasing a media session
150 // 1. If current media session's current state is idle, then terminate these steps.
151 if (m_currentState == State::Idle)
152 return;
153
154 // 2. If current media session still has one or more active participating media elements, then terminate these steps.
155 if (!m_activeParticipatingElements.isEmpty())
156 return;
157
158 // 3. Optionally, based on platform conventions, the user agent must release any currently held platform media focus
159 // for current media session.
160 // 4. Optionally, based on platform conventions, the user agent must remove any previously established ongoing media
161 // interface in the underlying platform’s notifications area and any ongoing media interface in the underlying
162 // platform's lock screen area for current media session, if any.
163 // 5. Optionally, based on platform conventions, the user agent must prevent any hardware and/or software media keys
164 // from controlling playback of current media session's active participating media elements.
165 // 6. Set current media session's current state to idle.
166 m_currentState = State::Idle;
167}
168
169bool MediaSession::invoke()
170{
171 // 4.4 Activating a media session
172 // 1. If we're already ACTIVE then return success.
173 if (m_currentState == State::Active)
174 return true;
175
176 // 2. Optionally, based on platform conventions, request the most appropriate platform-level media focus for media
177 // session based on its current media session type.
178
179 // 3. Run these substeps...
180
181 // 4. Set our current state to ACTIVE and return success.
182 m_currentState = State::Active;
183 return true;
184}
185
186void MediaSession::handleDuckInterruption()
187{
188 for (auto* element : m_activeParticipatingElements)
189 element->setShouldDuck(true);
190
191 m_currentState = State::Interrupted;
192}
193
194void MediaSession::handleUnduckInterruption()
195{
196 for (auto* element : m_activeParticipatingElements)
197 element->setShouldDuck(false);
198
199 m_currentState = State::Active;
200}
201
202void MediaSession::handleIndefinitePauseInterruption()
203{
204 safelyIterateActiveMediaElements([](HTMLMediaElement* element) {
205 element->pause();
206 });
207
208 m_activeParticipatingElements.clear();
209 m_currentState = State::Idle;
210}
211
212void MediaSession::handlePauseInterruption()
213{
214 m_currentState = State::Interrupted;
215
216 safelyIterateActiveMediaElements([](HTMLMediaElement* element) {
217 element->pause();
218 });
219}
220
221void MediaSession::handleUnpauseInterruption()
222{
223 m_currentState = State::Active;
224
225 safelyIterateActiveMediaElements([](HTMLMediaElement* element) {
226 element->play();
227 });
228}
229
230void MediaSession::togglePlayback()
231{
232 safelyIterateActiveMediaElements([](HTMLMediaElement* element) {
233 if (element->paused())
234 element->play();
235 else
236 element->pause();
237 });
238}
239
240void MediaSession::safelyIterateActiveMediaElements(const WTF::Function<void(HTMLMediaElement*)>& handler)
241{
242 ASSERT(!m_iteratedActiveParticipatingElements);
243
244 HashSet<HTMLMediaElement*> activeParticipatingElementsCopy = m_activeParticipatingElements;
245 m_iteratedActiveParticipatingElements = &activeParticipatingElementsCopy;
246
247 while (!activeParticipatingElementsCopy.isEmpty())
248 handler(activeParticipatingElementsCopy.takeAny());
249
250 m_iteratedActiveParticipatingElements = nullptr;
251}
252
253void MediaSession::skipToNextTrack()
254{
255 if (m_controls && m_controls->nextTrackEnabled())
256 m_controls->dispatchEvent(Event::create(eventNames().nexttrackEvent, Event::CanBubble::No, Event::IsCancelable::No));
257}
258
259void MediaSession::skipToPreviousTrack()
260{
261 if (m_controls && m_controls->previousTrackEnabled())
262 m_controls->dispatchEvent(Event::create(eventNames().previoustrackEvent, Event::CanBubble::No, Event::IsCancelable::No));
263}
264
265void MediaSession::controlIsEnabledDidChange()
266{
267 // Media remote controls are only allowed on Content media sessions.
268 ASSERT(m_kind == Kind::Content);
269
270 // Media elements belonging to Content media sessions have mutually-exclusive playback.
271 ASSERT(m_activeParticipatingElements.size() <= 1);
272
273 if (m_activeParticipatingElements.isEmpty())
274 return;
275
276 HTMLMediaElement* element = *m_activeParticipatingElements.begin();
277 m_document.updateIsPlayingMedia(element->elementID());
278}
279
280}
281
282#endif /* ENABLE(MEDIA_SESSION) */
283