1/*
2 * Copyright (C) 2016-2018 Apple Inc. All rights reserved.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this library; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 */
18
19#include "config.h"
20#include "UserMediaProcessManager.h"
21
22#if ENABLE(MEDIA_STREAM)
23
24#include "Logging.h"
25#include "MediaDeviceSandboxExtensions.h"
26#include "WebPageProxy.h"
27#include "WebProcessMessages.h"
28#include "WebProcessProxy.h"
29#include <WebCore/RealtimeMediaSourceCenter.h>
30#include <wtf/HashMap.h>
31#include <wtf/NeverDestroyed.h>
32
33namespace WebKit {
34
35#if ENABLE(SANDBOX_EXTENSIONS)
36static const ASCIILiteral audioExtensionPath { "com.apple.webkit.microphone"_s };
37static const ASCIILiteral videoExtensionPath { "com.apple.webkit.camera"_s };
38#endif
39
40static const Seconds deviceChangeDebounceTimerInterval { 200_ms };
41
42UserMediaProcessManager& UserMediaProcessManager::singleton()
43{
44 static NeverDestroyed<UserMediaProcessManager> manager;
45 return manager;
46}
47
48UserMediaProcessManager::UserMediaProcessManager()
49 : m_debounceTimer(RunLoop::main(), this, &UserMediaProcessManager::captureDevicesChanged)
50{
51}
52
53void UserMediaProcessManager::muteCaptureMediaStreamsExceptIn(WebPageProxy& pageStartingCapture)
54{
55#if PLATFORM(COCOA)
56 UserMediaPermissionRequestManagerProxy::forEach([&pageStartingCapture](auto& proxy) {
57 if (&proxy.page() != &pageStartingCapture)
58 proxy.page().setMediaStreamCaptureMuted(true);
59 });
60#else
61 UNUSED_PARAM(pageStartingCapture);
62#endif
63}
64
65bool UserMediaProcessManager::willCreateMediaStream(UserMediaPermissionRequestManagerProxy& proxy, bool withAudio, bool withVideo)
66{
67 ASSERT(withAudio || withVideo);
68
69 if (m_denyNextRequest) {
70 m_denyNextRequest = false;
71 return false;
72 }
73
74#if ENABLE(SANDBOX_EXTENSIONS) && USE(APPLE_INTERNAL_SDK)
75 auto& process = proxy.page().process();
76 size_t extensionCount = 0;
77
78 if (withAudio && !process.hasAudioCaptureExtension())
79 extensionCount++;
80 else
81 withAudio = false;
82
83 if (withVideo && !process.hasVideoCaptureExtension())
84 extensionCount++;
85 else
86 withVideo = false;
87
88 if (extensionCount) {
89 SandboxExtension::HandleArray handles;
90 Vector<String> ids;
91
92 if (!proxy.page().preferences().mockCaptureDevicesEnabled()) {
93 handles.allocate(extensionCount);
94 ids.reserveInitialCapacity(extensionCount);
95
96 if (withAudio && SandboxExtension::createHandleForGenericExtension(audioExtensionPath, handles[--extensionCount]))
97 ids.append(audioExtensionPath);
98
99 if (withVideo && SandboxExtension::createHandleForGenericExtension(videoExtensionPath, handles[--extensionCount]))
100 ids.append(videoExtensionPath);
101
102 if (ids.size() != handles.size()) {
103 WTFLogAlways("Could not create a required sandbox extension, capture will fail!");
104 return false;
105 }
106 }
107
108 for (const auto& id : ids)
109 RELEASE_LOG(WebRTC, "UserMediaProcessManager::willCreateMediaStream - granting extension %s", id.utf8().data());
110
111 if (withAudio)
112 process.grantAudioCaptureExtension();
113 if (withVideo)
114 process.grantVideoCaptureExtension();
115 process.send(Messages::WebProcess::GrantUserMediaDeviceSandboxExtensions(MediaDeviceSandboxExtensions(ids, WTFMove(handles))), 0);
116 }
117#else
118 UNUSED_PARAM(proxy);
119 UNUSED_PARAM(withAudio);
120 UNUSED_PARAM(withVideo);
121#endif
122
123 proxy.page().activateMediaStreamCaptureInPage();
124
125 return true;
126}
127
128void UserMediaProcessManager::revokeSandboxExtensionsIfNeeded(WebProcessProxy& process)
129{
130#if ENABLE(SANDBOX_EXTENSIONS)
131 bool hasAudioCapture = false;
132 bool hasVideoCapture = false;
133
134 UserMediaPermissionRequestManagerProxy::forEach([&hasAudioCapture, &hasVideoCapture, &process](auto& managerProxy) {
135 if (&process != &managerProxy.page().process())
136 return;
137 hasAudioCapture |= managerProxy.page().isCapturingAudio();
138 hasVideoCapture |= managerProxy.page().isCapturingVideo();
139 });
140
141 if (hasAudioCapture && hasVideoCapture)
142 return;
143
144 Vector<String> params;
145 if (!hasAudioCapture && process.hasAudioCaptureExtension()) {
146 params.append(audioExtensionPath);
147 process.revokeAudioCaptureExtension();
148 }
149 if (!hasVideoCapture && process.hasVideoCaptureExtension()) {
150 params.append(videoExtensionPath);
151 process.revokeVideoCaptureExtension();
152 }
153
154 if (params.isEmpty())
155 return;
156
157 for (const auto& id : params)
158 RELEASE_LOG(WebRTC, "UserMediaProcessManager::endedCaptureSession - revoking extension %s", id.utf8().data());
159
160 process.send(Messages::WebProcess::RevokeUserMediaDeviceSandboxExtensions(params), 0);
161#endif
162}
163
164void UserMediaProcessManager::setCaptureEnabled(bool enabled)
165{
166 if (enabled == m_captureEnabled)
167 return;
168
169 m_captureEnabled = enabled;
170
171 if (enabled)
172 return;
173
174 UserMediaPermissionRequestManagerProxy::forEach([](auto& proxy) {
175 proxy.stopCapture();
176 });
177}
178
179void UserMediaProcessManager::captureDevicesChanged()
180{
181 UserMediaPermissionRequestManagerProxy::forEach([](auto& proxy) {
182 proxy.captureDevicesChanged();
183 });
184}
185
186void UserMediaProcessManager::beginMonitoringCaptureDevices()
187{
188 static std::once_flag onceFlag;
189
190 std::call_once(onceFlag, [this] {
191 m_captureDevices = WebCore::RealtimeMediaSourceCenter::singleton().getMediaStreamDevices();
192
193 WebCore::RealtimeMediaSourceCenter::singleton().setDevicesChangedObserver([this]() {
194 auto oldDevices = WTFMove(m_captureDevices);
195 m_captureDevices = WebCore::RealtimeMediaSourceCenter::singleton().getMediaStreamDevices();
196
197 if (m_captureDevices.size() == oldDevices.size()) {
198 bool haveChanges = false;
199 for (auto &newDevice : m_captureDevices) {
200 if (newDevice.type() != WebCore::CaptureDevice::DeviceType::Camera && newDevice.type() != WebCore::CaptureDevice::DeviceType::Microphone)
201 continue;
202
203 auto index = oldDevices.findMatching([&newDevice] (auto& oldDevice) {
204 return newDevice.persistentId() == oldDevice.persistentId() && newDevice.enabled() != oldDevice.enabled();
205 });
206
207 if (index == notFound) {
208 haveChanges = true;
209 break;
210 }
211 }
212
213 if (!haveChanges)
214 return;
215 }
216
217 // When a device with camera and microphone is attached or detached, the CaptureDevice notification for
218 // the different devices won't arrive at the same time so delay a bit so we can coalesce the callbacks.
219 if (!m_debounceTimer.isActive())
220 m_debounceTimer.startOneShot(deviceChangeDebounceTimerInterval);
221 });
222 });
223}
224
225} // namespace WebKit
226
227#endif
228