Move more logic from AudioDestinationNode to its subclasses
[WebKit-https.git] / Source / WebCore / Modules / webaudio / AudioContext.cpp
1 /*
2  * Copyright (C) 2010 Google Inc. All rights reserved.
3  * Copyright (C) 2016-2021 Apple Inc. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1.  Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2.  Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND ANY
15  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17  * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY
18  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19  * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20  * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
21  * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #include "config.h"
27
28 #if ENABLE(WEB_AUDIO)
29
30 #include "AudioContext.h"
31 #include "AudioContextOptions.h"
32 #include "AudioTimestamp.h"
33 #include "DOMWindow.h"
34 #include "JSDOMPromiseDeferred.h"
35 #include "Logging.h"
36 #include "Page.h"
37 #include "Performance.h"
38 #include "PlatformMediaSessionManager.h"
39 #include "Quirks.h"
40 #include <wtf/IsoMallocInlines.h>
41
42 #if ENABLE(MEDIA_STREAM)
43 #include "MediaStream.h"
44 #include "MediaStreamAudioDestinationNode.h"
45 #include "MediaStreamAudioSource.h"
46 #include "MediaStreamAudioSourceNode.h"
47 #include "MediaStreamAudioSourceOptions.h"
48 #endif
49
50 #if ENABLE(VIDEO)
51 #include "HTMLMediaElement.h"
52 #include "MediaElementAudioSourceNode.h"
53 #include "MediaElementAudioSourceOptions.h"
54 #endif
55
56 namespace WebCore {
57
58 #define RELEASE_LOG_IF_ALLOWED(fmt, ...) RELEASE_LOG_IF(document() && document()->page() && document()->page()->isAlwaysOnLoggingAllowed(), Media, "%p - AudioContext::" fmt, this, ##__VA_ARGS__)
59
60 #if OS(WINDOWS)
61 // Don't allow more than this number of simultaneous AudioContexts talking to hardware.
62 constexpr unsigned maxHardwareContexts = 4;
63 #endif
64
65 WTF_MAKE_ISO_ALLOCATED_IMPL(AudioContext);
66
67 #if OS(WINDOWS)
68 static unsigned hardwareContextCount;
69 #endif
70
71 static Optional<float>& defaultSampleRateForTesting()
72 {
73     static Optional<float> sampleRate;
74     return sampleRate;
75 }
76
77 static bool shouldDocumentAllowWebAudioToAutoPlay(const Document& document)
78 {
79     if (document.processingUserGestureForMedia() || document.isCapturing())
80         return true;
81     return document.quirks().shouldAutoplayWebAudioForArbitraryUserGesture() && document.topDocument().hasHadUserInteraction();
82 }
83
84 void AudioContext::setDefaultSampleRateForTesting(Optional<float> sampleRate)
85 {
86     defaultSampleRateForTesting() = sampleRate;
87 }
88
89 ExceptionOr<Ref<AudioContext>> AudioContext::create(Document& document, AudioContextOptions&& contextOptions)
90 {
91     ASSERT(isMainThread());
92 #if OS(WINDOWS)
93     if (hardwareContextCount >= maxHardwareContexts)
94         return Exception { QuotaExceededError, "Reached maximum number of hardware contexts on this platform"_s };
95 #endif
96     
97     if (!document.isFullyActive())
98         return Exception { InvalidStateError, "Document is not fully active"_s };
99     
100     // FIXME: Figure out where latencyHint should go.
101
102     if (!contextOptions.sampleRate && defaultSampleRateForTesting())
103         contextOptions.sampleRate = *defaultSampleRateForTesting();
104
105     if (contextOptions.sampleRate.hasValue() && !isSupportedSampleRate(contextOptions.sampleRate.value()))
106         return Exception { SyntaxError, "sampleRate is not in range"_s };
107     
108     auto audioContext = adoptRef(*new AudioContext(document, contextOptions));
109     audioContext->suspendIfNeeded();
110     return audioContext;
111 }
112
113 AudioContext::AudioContext(Document& document, const AudioContextOptions& contextOptions)
114     : BaseAudioContext(document)
115     , m_destinationNode(makeUniqueRef<DefaultAudioDestinationNode>(*this, contextOptions.sampleRate))
116     , m_mediaSession(PlatformMediaSession::create(PlatformMediaSessionManager::sharedManager(), *this))
117 {
118     // According to spec AudioContext must die only after page navigate.
119     // Lets mark it as ActiveDOMObject with pending activity and unmark it in clear method.
120     setPendingActivity();
121
122     constructCommon();
123
124     // Initialize the destination node's muted state to match the page's current muted state.
125     pageMutedStateDidChange();
126
127     document.addAudioProducer(*this);
128     document.registerForVisibilityStateChangedCallbacks(*this);
129
130     // Unlike OfflineAudioContext, AudioContext does not require calling resume() to start rendering.
131     // Lazy initialization starts rendering so we schedule a task here to make sure lazy initialization
132     // ends up happening, even if no audio node gets constructed.
133     postTask([this] {
134         if (!isStopped())
135             lazyInitialize();
136     });
137 }
138
139 void AudioContext::constructCommon()
140 {
141     ASSERT(document());
142     if (document()->audioPlaybackRequiresUserGesture())
143         addBehaviorRestriction(RequireUserGestureForAudioStartRestriction);
144     else
145         m_restrictions = NoRestrictions;
146
147 #if PLATFORM(COCOA)
148     addBehaviorRestriction(RequirePageConsentForAudioStartRestriction);
149 #endif
150 }
151
152 AudioContext::~AudioContext()
153 {
154     if (!isOfflineContext() && scriptExecutionContext()) {
155         document()->removeAudioProducer(*this);
156         document()->unregisterForVisibilityStateChangedCallbacks(*this);
157     }
158 }
159
160 void AudioContext::uninitialize()
161 {
162     if (!isInitialized())
163         return;
164
165     BaseAudioContext::uninitialize();
166
167 #if OS(WINDOWS)
168     ASSERT(hardwareContextCount);
169     --hardwareContextCount;
170 #endif
171
172     setState(State::Closed);
173 }
174
175 double AudioContext::baseLatency()
176 {
177     lazyInitialize();
178
179     return static_cast<double>(destination().framesPerBuffer()) / sampleRate();
180 }
181
182 AudioTimestamp AudioContext::getOutputTimestamp(DOMWindow& window)
183 {
184     auto& performance = window.performance();
185
186     auto position = outputPosition();
187
188     // The timestamp of what is currently being played (contextTime) cannot be
189     // later than what is being rendered. (currentTime)
190     position.position = Seconds { std::min(position.position.seconds(), currentTime()) };
191
192     auto performanceTime = performance.relativeTimeFromTimeOriginInReducedResolution(position.timestamp);
193     performanceTime = std::max(performanceTime, 0.0);
194
195     return { position.position.seconds(), performanceTime };
196 }
197
198 void AudioContext::close(DOMPromiseDeferred<void>&& promise)
199 {
200     if (isOfflineContext() || isStopped()) {
201         promise.reject(InvalidStateError);
202         return;
203     }
204
205     if (state() == State::Closed) {
206         promise.resolve();
207         return;
208     }
209
210     addReaction(State::Closed, WTFMove(promise));
211
212     lazyInitialize();
213
214     destination().close([this, activity = makePendingActivity(*this)] {
215         setState(State::Closed);
216         uninitialize();
217         m_mediaSession->setActive(false);
218     });
219 }
220
221 void AudioContext::suspendRendering(DOMPromiseDeferred<void>&& promise)
222 {
223     if (isOfflineContext()) {
224         promise.reject(Exception { InvalidStateError, "Cannot call suspend() on an OfflineAudioContext"_s });
225         return;
226     }
227
228     if (isStopped() || state() == State::Closed) {
229         promise.reject(Exception { InvalidStateError, "Context is closed"_s });
230         return;
231     }
232
233     m_wasSuspendedByScript = true;
234
235     if (!willPausePlayback()) {
236         addReaction(State::Suspended, WTFMove(promise));
237         return;
238     }
239
240     lazyInitialize();
241
242     destination().suspend([this, activity = makePendingActivity(*this), promise = WTFMove(promise)](Optional<Exception>&& exception) mutable {
243         if (exception) {
244             promise.reject(WTFMove(*exception));
245             return;
246         }
247         setState(State::Suspended);
248         promise.resolve();
249     });
250 }
251
252 void AudioContext::resumeRendering(DOMPromiseDeferred<void>&& promise)
253 {
254     if (isOfflineContext()) {
255         promise.reject(Exception { InvalidStateError, "Cannot call resume() on an OfflineAudioContext"_s });
256         return;
257     }
258
259     if (isStopped() || state() == State::Closed) {
260         promise.reject(Exception { InvalidStateError, "Context is closed"_s });
261         return;
262     }
263
264     m_wasSuspendedByScript = false;
265
266     if (!willBeginPlayback()) {
267         addReaction(State::Running, WTFMove(promise));
268         return;
269     }
270
271     lazyInitialize();
272
273     destination().resume([this, activity = makePendingActivity(*this), promise = WTFMove(promise)](Optional<Exception>&& exception) mutable {
274         if (exception) {
275             promise.reject(WTFMove(*exception));
276             return;
277         }
278
279         // Since we update the state asynchronously, we may have been interrupted after the
280         // call to resume() and before this lambda runs. In this case, we don't want to
281         // reset the state to running.
282         bool interrupted = m_mediaSession->state() == PlatformMediaSession::Interrupted;
283         setState(interrupted ? State::Interrupted : State::Running);
284         if (interrupted)
285             addReaction(State::Running, WTFMove(promise));
286         else
287             promise.resolve();
288     });
289 }
290
291 void AudioContext::sourceNodeWillBeginPlayback(AudioNode& audioNode)
292 {
293     BaseAudioContext::sourceNodeWillBeginPlayback(audioNode);
294
295     // Called by scheduled AudioNodes when clients schedule their start times.
296     // Prior to the introduction of suspend(), resume(), and stop(), starting
297     // a scheduled AudioNode would remove the user-gesture restriction, if present,
298     // and would thus unmute the context. Now that AudioContext stays in the
299     // "suspended" state if a user-gesture restriction is present, starting a
300     // schedule AudioNode should set the state to "running", but only if the
301     // user-gesture restriction is set.
302     if (userGestureRequiredForAudioStart())
303         startRendering();
304 }
305
306 void AudioContext::startRendering()
307 {
308     ALWAYS_LOG(LOGIDENTIFIER);
309     if (isStopped() || !willBeginPlayback() || m_wasSuspendedByScript)
310         return;
311
312     setPendingActivity();
313
314     lazyInitialize();
315     destination().startRendering([this, protectedThis = makeRef(*this)](Optional<Exception>&& exception) {
316         if (!exception)
317             setState(State::Running);
318     });
319 }
320
321 void AudioContext::lazyInitialize()
322 {
323     if (isInitialized())
324         return;
325
326     BaseAudioContext::lazyInitialize();
327     if (isInitialized()) {
328         if (state() != State::Running) {
329             // This starts the audio thread. The destination node's provideInput() method will now be called repeatedly to render audio.
330             // Each time provideInput() is called, a portion of the audio stream is rendered. Let's call this time period a "render quantum".
331             // NOTE: for now default AudioContext does not need an explicit startRendering() call from JavaScript.
332             // We may want to consider requiring it for symmetry with OfflineAudioContext.
333             startRendering();
334 #if OS(WINDOWS)
335             ++hardwareContextCount;
336 #endif
337         }
338     }
339 }
340
341 bool AudioContext::willPausePlayback()
342 {
343     auto* document = this->document();
344     if (!document)
345         return false;
346
347     if (userGestureRequiredForAudioStart()) {
348         if (!document->processingUserGestureForMedia())
349             return false;
350         removeBehaviorRestriction(RequireUserGestureForAudioStartRestriction);
351     }
352
353     if (pageConsentRequiredForAudioStart()) {
354         auto* page = document->page();
355         if (page && !page->canStartMedia()) {
356             document->addMediaCanStartListener(*this);
357             return false;
358         }
359         removeBehaviorRestriction(RequirePageConsentForAudioStartRestriction);
360     }
361
362     return m_mediaSession->clientWillPausePlayback();
363 }
364
365 MediaProducer::MediaStateFlags AudioContext::mediaState() const
366 {
367     if (!isStopped() && destination().isPlayingAudio())
368         return MediaProducer::MediaState::IsPlayingAudio;
369
370     return MediaProducer::IsNotPlaying;
371 }
372
373 void AudioContext::mayResumePlayback(bool shouldResume)
374 {
375     if (state() == State::Closed || !isInitialized() || state() == State::Running)
376         return;
377
378     if (!shouldResume) {
379         setState(State::Suspended);
380         return;
381     }
382
383     if (!willBeginPlayback())
384         return;
385
386     lazyInitialize();
387
388     destination().resume([this, protectedThis = makeRef(*this)](Optional<Exception>&& exception) {
389         setState(exception ? State::Suspended : State::Running);
390     });
391 }
392
393 bool AudioContext::willBeginPlayback()
394 {
395     auto* document = this->document();
396     if (!document)
397         return false;
398
399     if (userGestureRequiredForAudioStart()) {
400         if (!shouldDocumentAllowWebAudioToAutoPlay(*document)) {
401             ALWAYS_LOG(LOGIDENTIFIER, "returning false, not processing user gesture or capturing");
402             return false;
403         }
404         removeBehaviorRestriction(RequireUserGestureForAudioStartRestriction);
405     }
406
407     if (pageConsentRequiredForAudioStart()) {
408         auto* page = document->page();
409         if (page && !page->canStartMedia()) {
410             document->addMediaCanStartListener(*this);
411             ALWAYS_LOG(LOGIDENTIFIER, "returning false, page doesn't allow media to start");
412             return false;
413         }
414         removeBehaviorRestriction(RequirePageConsentForAudioStartRestriction);
415     }
416
417     m_mediaSession->setActive(true);
418
419     auto willBegin = m_mediaSession->clientWillBeginPlayback();
420     ALWAYS_LOG(LOGIDENTIFIER, "returning ", willBegin);
421
422     return willBegin;
423 }
424
425 void AudioContext::visibilityStateChanged()
426 {
427     // Do not suspend if audio is audible.
428     if (!document() || mediaState() == MediaProducer::MediaState::IsPlayingAudio || isStopped())
429         return;
430
431     if (document()->hidden()) {
432         if (state() == State::Running) {
433             ALWAYS_LOG(LOGIDENTIFIER, "Suspending playback after going to the background");
434             m_mediaSession->beginInterruption(PlatformMediaSession::EnteringBackground);
435         }
436     } else {
437         if (state() == State::Interrupted) {
438             ALWAYS_LOG(LOGIDENTIFIER, "Resuming playback after entering foreground");
439             m_mediaSession->endInterruption(PlatformMediaSession::MayResumePlaying);
440         }
441     }
442 }
443
444 void AudioContext::suspend(ReasonForSuspension)
445 {
446     if (isClosed() || m_wasSuspendedByScript)
447         return;
448
449     m_mediaSession->beginInterruption(PlatformMediaSession::PlaybackSuspended);
450     document()->updateIsPlayingMedia();
451 }
452
453 void AudioContext::resume()
454 {
455     if (isClosed() || m_wasSuspendedByScript)
456         return;
457
458     m_mediaSession->endInterruption(PlatformMediaSession::MayResumePlaying);
459     document()->updateIsPlayingMedia();
460 }
461
462 const char* AudioContext::activeDOMObjectName() const
463 {
464     return "AudioContext";
465 }
466
467 void AudioContext::suspendPlayback()
468 {
469     if (state() == State::Closed || !isInitialized())
470         return;
471
472     lazyInitialize();
473
474     destination().suspend([this, protectedThis = makeRef(*this)](Optional<Exception>&& exception) {
475         if (exception)
476             return;
477
478         bool interrupted = m_mediaSession->state() == PlatformMediaSession::Interrupted;
479         setState(interrupted ? State::Interrupted : State::Suspended);
480     });
481 }
482
483 MediaSessionGroupIdentifier AudioContext::mediaSessionGroupIdentifier() const
484 {
485     auto* document = downcast<Document>(m_scriptExecutionContext);
486     return document && document->page() ? document->page()->mediaSessionGroupIdentifier() : MediaSessionGroupIdentifier { };
487 }
488
489 bool AudioContext::isSuspended() const
490 {
491     return !document() || document()->activeDOMObjectsAreSuspended() || document()->activeDOMObjectsAreStopped();
492 }
493
494 void AudioContext::pageMutedStateDidChange()
495 {
496     if (document() && document()->page())
497         destination().setMuted(document()->page()->isAudioMuted());
498 }
499
500 void AudioContext::mediaCanStart(Document& document)
501 {
502     ASSERT_UNUSED(document, &document == this->document());
503     removeBehaviorRestriction(RequirePageConsentForAudioStartRestriction);
504     mayResumePlayback(true);
505 }
506
507 void AudioContext::isPlayingAudioDidChange()
508 {
509     // Heap allocations are forbidden on the audio thread for performance reasons so we need to
510     // explicitly allow the following allocation(s).
511     DisableMallocRestrictionsForCurrentThreadScope disableMallocRestrictions;
512
513     // Make sure to call Document::updateIsPlayingMedia() on the main thread, since
514     // we could be on the audio I/O thread here and the call into WebCore could block.
515     callOnMainThread([protectedThis = makeRef(*this)] {
516         if (auto* document = protectedThis->document())
517             document->updateIsPlayingMedia();
518     });
519 }
520
521 #if !RELEASE_LOG_DISABLED
522 const Logger& AudioContext::logger() const
523 {
524     return BaseAudioContext::logger();
525 }
526 #endif
527
528 #if ENABLE(VIDEO)
529
530 ExceptionOr<Ref<MediaElementAudioSourceNode>> AudioContext::createMediaElementSource(HTMLMediaElement& mediaElement)
531 {
532     ALWAYS_LOG(LOGIDENTIFIER);
533
534     ASSERT(isMainThread());
535     return MediaElementAudioSourceNode::create(*this, { &mediaElement });
536 }
537
538 #endif
539
540 #if ENABLE(MEDIA_STREAM)
541
542 ExceptionOr<Ref<MediaStreamAudioSourceNode>> AudioContext::createMediaStreamSource(MediaStream& mediaStream)
543 {
544     ALWAYS_LOG(LOGIDENTIFIER);
545
546     ASSERT(isMainThread());
547
548     return MediaStreamAudioSourceNode::create(*this, { &mediaStream });
549 }
550
551 ExceptionOr<Ref<MediaStreamAudioDestinationNode>> AudioContext::createMediaStreamDestination()
552 {
553     return MediaStreamAudioDestinationNode::create(*this);
554 }
555
556 #endif
557
558 } // namespace WebCore
559
560 #endif // ENABLE(WEB_AUDIO)