1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /* vim:expandtab:shiftwidth=4:tabstop=4:
3 */
4 /* This Source Code Form is subject to the terms of the Mozilla Public
5 * License, v. 2.0. If a copy of the MPL was not distributed with this
6 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7
8 #include <string.h>
9
10 #include "nscore.h"
11 #include "plstr.h"
12 #include "prlink.h"
13
14 #include "nsSound.h"
15
16 #include "HeadlessSound.h"
17 #include "nsIURL.h"
18 #include "nsIFileURL.h"
19 #include "nsNetUtil.h"
20 #include "nsIChannel.h"
21 #include "nsCOMPtr.h"
22 #include "nsString.h"
23 #include "nsDirectoryService.h"
24 #include "nsDirectoryServiceDefs.h"
25 #include "mozilla/FileUtils.h"
26 #include "mozilla/Services.h"
27 #include "mozilla/Unused.h"
28 #include "mozilla/WidgetUtils.h"
29 #include "nsIXULAppInfo.h"
30 #include "nsContentUtils.h"
31 #include "gfxPlatform.h"
32 #include "mozilla/ClearOnShutdown.h"
33
34 #include <stdio.h>
35 #include <unistd.h>
36
37 #include <gtk/gtk.h>
38 static PRLibrary *libcanberra = nullptr;
39
40 /* used to play sounds with libcanberra. */
41 typedef struct _ca_context ca_context;
42 typedef struct _ca_proplist ca_proplist;
43
44 typedef void (*ca_finish_callback_t)(ca_context *c, uint32_t id, int error_code,
45 void *userdata);
46
47 typedef int (*ca_context_create_fn)(ca_context **);
48 typedef int (*ca_context_destroy_fn)(ca_context *);
49 typedef int (*ca_context_play_fn)(ca_context *c, uint32_t id, ...);
50 typedef int (*ca_context_change_props_fn)(ca_context *c, ...);
51 typedef int (*ca_proplist_create_fn)(ca_proplist **);
52 typedef int (*ca_proplist_destroy_fn)(ca_proplist *);
53 typedef int (*ca_proplist_sets_fn)(ca_proplist *c, const char *key,
54 const char *value);
55 typedef int (*ca_context_play_full_fn)(ca_context *c, uint32_t id,
56 ca_proplist *p, ca_finish_callback_t cb,
57 void *userdata);
58
59 static ca_context_create_fn ca_context_create;
60 static ca_context_destroy_fn ca_context_destroy;
61 static ca_context_play_fn ca_context_play;
62 static ca_context_change_props_fn ca_context_change_props;
63 static ca_proplist_create_fn ca_proplist_create;
64 static ca_proplist_destroy_fn ca_proplist_destroy;
65 static ca_proplist_sets_fn ca_proplist_sets;
66 static ca_context_play_full_fn ca_context_play_full;
67
68 struct ScopedCanberraFile {
ScopedCanberraFileScopedCanberraFile69 explicit ScopedCanberraFile(nsIFile *file) : mFile(file){};
70
~ScopedCanberraFileScopedCanberraFile71 ~ScopedCanberraFile() {
72 if (mFile) {
73 mFile->Remove(false);
74 }
75 }
76
forgetScopedCanberraFile77 void forget() { mozilla::Unused << mFile.forget(); }
operator ->ScopedCanberraFile78 nsIFile *operator->() { return mFile; }
operator nsIFile*ScopedCanberraFile79 operator nsIFile *() { return mFile; }
80
81 nsCOMPtr<nsIFile> mFile;
82 };
83
ca_context_get_default()84 static ca_context *ca_context_get_default() {
85 // This allows us to avoid race conditions with freeing the context by handing
86 // that responsibility to Glib, and still use one context at a time
87 static GStaticPrivate ctx_static_private = G_STATIC_PRIVATE_INIT;
88
89 ca_context *ctx = (ca_context *)g_static_private_get(&ctx_static_private);
90
91 if (ctx) {
92 return ctx;
93 }
94
95 ca_context_create(&ctx);
96 if (!ctx) {
97 return nullptr;
98 }
99
100 g_static_private_set(&ctx_static_private, ctx,
101 (GDestroyNotify)ca_context_destroy);
102
103 GtkSettings *settings = gtk_settings_get_default();
104 if (g_object_class_find_property(G_OBJECT_GET_CLASS(settings),
105 "gtk-sound-theme-name")) {
106 gchar *sound_theme_name = nullptr;
107 g_object_get(settings, "gtk-sound-theme-name", &sound_theme_name, nullptr);
108
109 if (sound_theme_name) {
110 ca_context_change_props(ctx, "canberra.xdg-theme.name", sound_theme_name,
111 nullptr);
112 g_free(sound_theme_name);
113 }
114 }
115
116 nsAutoString wbrand;
117 mozilla::widget::WidgetUtils::GetBrandShortName(wbrand);
118 ca_context_change_props(ctx, "application.name",
119 NS_ConvertUTF16toUTF8(wbrand).get(), nullptr);
120
121 nsCOMPtr<nsIXULAppInfo> appInfo =
122 do_GetService("@mozilla.org/xre/app-info;1");
123 if (appInfo) {
124 nsAutoCString version;
125 appInfo->GetVersion(version);
126
127 ca_context_change_props(ctx, "application.version", version.get(), nullptr);
128 }
129
130 ca_context_change_props(ctx, "application.icon_name", MOZ_APP_NAME, nullptr);
131
132 return ctx;
133 }
134
ca_finish_cb(ca_context * c,uint32_t id,int error_code,void * userdata)135 static void ca_finish_cb(ca_context *c, uint32_t id, int error_code,
136 void *userdata) {
137 nsIFile *file = reinterpret_cast<nsIFile *>(userdata);
138 if (file) {
139 file->Remove(false);
140 NS_RELEASE(file);
141 }
142 }
143
NS_IMPL_ISUPPORTS(nsSound,nsISound,nsIStreamLoaderObserver)144 NS_IMPL_ISUPPORTS(nsSound, nsISound, nsIStreamLoaderObserver)
145
146 ////////////////////////////////////////////////////////////////////////
147 nsSound::nsSound() { mInited = false; }
148
~nsSound()149 nsSound::~nsSound() {}
150
151 NS_IMETHODIMP
Init()152 nsSound::Init() {
153 // This function is designed so that no library is compulsory, and
154 // one library missing doesn't cause the other(s) to not be used.
155 if (mInited) return NS_OK;
156
157 mInited = true;
158
159 if (!libcanberra) {
160 libcanberra = PR_LoadLibrary("libcanberra.so.0");
161 if (libcanberra) {
162 ca_context_create = (ca_context_create_fn)PR_FindFunctionSymbol(
163 libcanberra, "ca_context_create");
164 if (!ca_context_create) {
165 PR_UnloadLibrary(libcanberra);
166 libcanberra = nullptr;
167 } else {
168 // at this point we know we have a good libcanberra library
169 ca_context_destroy = (ca_context_destroy_fn)PR_FindFunctionSymbol(
170 libcanberra, "ca_context_destroy");
171 ca_context_play = (ca_context_play_fn)PR_FindFunctionSymbol(
172 libcanberra, "ca_context_play");
173 ca_context_change_props =
174 (ca_context_change_props_fn)PR_FindFunctionSymbol(
175 libcanberra, "ca_context_change_props");
176 ca_proplist_create = (ca_proplist_create_fn)PR_FindFunctionSymbol(
177 libcanberra, "ca_proplist_create");
178 ca_proplist_destroy = (ca_proplist_destroy_fn)PR_FindFunctionSymbol(
179 libcanberra, "ca_proplist_destroy");
180 ca_proplist_sets = (ca_proplist_sets_fn)PR_FindFunctionSymbol(
181 libcanberra, "ca_proplist_sets");
182 ca_context_play_full = (ca_context_play_full_fn)PR_FindFunctionSymbol(
183 libcanberra, "ca_context_play_full");
184 }
185 }
186 }
187
188 return NS_OK;
189 }
190
Shutdown()191 /* static */ void nsSound::Shutdown() {
192 if (libcanberra) {
193 PR_UnloadLibrary(libcanberra);
194 libcanberra = nullptr;
195 }
196 }
197
198 namespace mozilla {
199 namespace sound {
200 StaticRefPtr<nsISound> sInstance;
201 }
202 } // namespace mozilla
GetInstance()203 /* static */ already_AddRefed<nsISound> nsSound::GetInstance() {
204 using namespace mozilla::sound;
205
206 if (!sInstance) {
207 if (gfxPlatform::IsHeadless()) {
208 sInstance = new mozilla::widget::HeadlessSound();
209 } else {
210 sInstance = new nsSound();
211 }
212 ClearOnShutdown(&sInstance);
213 }
214
215 RefPtr<nsISound> service = sInstance.get();
216 return service.forget();
217 }
218
OnStreamComplete(nsIStreamLoader * aLoader,nsISupports * context,nsresult aStatus,uint32_t dataLen,const uint8_t * data)219 NS_IMETHODIMP nsSound::OnStreamComplete(nsIStreamLoader *aLoader,
220 nsISupports *context, nsresult aStatus,
221 uint32_t dataLen, const uint8_t *data) {
222 // print a load error on bad status, and return
223 if (NS_FAILED(aStatus)) {
224 #ifdef DEBUG
225 if (aLoader) {
226 nsCOMPtr<nsIRequest> request;
227 aLoader->GetRequest(getter_AddRefs(request));
228 if (request) {
229 nsCOMPtr<nsIURI> uri;
230 nsCOMPtr<nsIChannel> channel = do_QueryInterface(request);
231 if (channel) {
232 channel->GetURI(getter_AddRefs(uri));
233 if (uri) {
234 printf("Failed to load %s\n", uri->GetSpecOrDefault().get());
235 }
236 }
237 }
238 }
239 #endif
240 return aStatus;
241 }
242
243 nsCOMPtr<nsIFile> tmpFile;
244 nsDirectoryService::gService->Get(NS_OS_TEMP_DIR, NS_GET_IID(nsIFile),
245 getter_AddRefs(tmpFile));
246
247 nsresult rv =
248 tmpFile->AppendNative(nsDependentCString("mozilla_audio_sample"));
249 if (NS_FAILED(rv)) {
250 return rv;
251 }
252
253 rv = tmpFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, PR_IRUSR | PR_IWUSR);
254 if (NS_FAILED(rv)) {
255 return rv;
256 }
257
258 ScopedCanberraFile canberraFile(tmpFile);
259
260 mozilla::AutoFDClose fd;
261 rv = canberraFile->OpenNSPRFileDesc(PR_WRONLY, PR_IRUSR | PR_IWUSR,
262 &fd.rwget());
263 if (NS_FAILED(rv)) {
264 return rv;
265 }
266
267 // XXX: Should we do this on another thread?
268 uint32_t length = dataLen;
269 while (length > 0) {
270 int32_t amount = PR_Write(fd, data, length);
271 if (amount < 0) {
272 return NS_ERROR_FAILURE;
273 }
274 length -= amount;
275 data += amount;
276 }
277
278 ca_context *ctx = ca_context_get_default();
279 if (!ctx) {
280 return NS_ERROR_OUT_OF_MEMORY;
281 }
282
283 ca_proplist *p;
284 ca_proplist_create(&p);
285 if (!p) {
286 return NS_ERROR_OUT_OF_MEMORY;
287 }
288
289 nsAutoCString path;
290 rv = canberraFile->GetNativePath(path);
291 if (NS_FAILED(rv)) {
292 return rv;
293 }
294
295 ca_proplist_sets(p, "media.filename", path.get());
296 if (ca_context_play_full(ctx, 0, p, ca_finish_cb, canberraFile) >= 0) {
297 // Don't delete the temporary file here if ca_context_play_full succeeds
298 canberraFile.forget();
299 }
300 ca_proplist_destroy(p);
301
302 return NS_OK;
303 }
304
Beep()305 NS_IMETHODIMP nsSound::Beep() {
306 ::gdk_beep();
307 return NS_OK;
308 }
309
Play(nsIURL * aURL)310 NS_IMETHODIMP nsSound::Play(nsIURL *aURL) {
311 if (!mInited) Init();
312
313 if (!libcanberra) return NS_ERROR_NOT_AVAILABLE;
314
315 bool isFile;
316 nsresult rv = aURL->SchemeIs("file", &isFile);
317 if (NS_SUCCEEDED(rv) && isFile) {
318 ca_context *ctx = ca_context_get_default();
319 if (!ctx) {
320 return NS_ERROR_OUT_OF_MEMORY;
321 }
322
323 nsAutoCString spec;
324 rv = aURL->GetSpec(spec);
325 if (NS_FAILED(rv)) {
326 return rv;
327 }
328 gchar *path = g_filename_from_uri(spec.get(), nullptr, nullptr);
329 if (!path) {
330 return NS_ERROR_FILE_UNRECOGNIZED_PATH;
331 }
332
333 ca_context_play(ctx, 0, "media.filename", path, nullptr);
334 g_free(path);
335 } else {
336 nsCOMPtr<nsIStreamLoader> loader;
337 rv = NS_NewStreamLoader(getter_AddRefs(loader), aURL,
338 this, // aObserver
339 nsContentUtils::GetSystemPrincipal(),
340 nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
341 nsIContentPolicy::TYPE_OTHER);
342 }
343
344 return rv;
345 }
346
PlayEventSound(uint32_t aEventId)347 NS_IMETHODIMP nsSound::PlayEventSound(uint32_t aEventId) {
348 if (!mInited) Init();
349
350 if (!libcanberra) return NS_OK;
351
352 // Do we even want alert sounds?
353 GtkSettings *settings = gtk_settings_get_default();
354
355 if (g_object_class_find_property(G_OBJECT_GET_CLASS(settings),
356 "gtk-enable-event-sounds")) {
357 gboolean enable_sounds = TRUE;
358 g_object_get(settings, "gtk-enable-event-sounds", &enable_sounds, nullptr);
359
360 if (!enable_sounds) {
361 return NS_OK;
362 }
363 }
364
365 ca_context *ctx = ca_context_get_default();
366 if (!ctx) {
367 return NS_ERROR_OUT_OF_MEMORY;
368 }
369
370 switch (aEventId) {
371 case EVENT_ALERT_DIALOG_OPEN:
372 ca_context_play(ctx, 0, "event.id", "dialog-warning", nullptr);
373 break;
374 case EVENT_CONFIRM_DIALOG_OPEN:
375 ca_context_play(ctx, 0, "event.id", "dialog-question", nullptr);
376 break;
377 case EVENT_NEW_MAIL_RECEIVED:
378 ca_context_play(ctx, 0, "event.id", "message-new-email", nullptr);
379 break;
380 case EVENT_MENU_EXECUTE:
381 ca_context_play(ctx, 0, "event.id", "menu-click", nullptr);
382 break;
383 case EVENT_MENU_POPUP:
384 ca_context_play(ctx, 0, "event.id", "menu-popup", nullptr);
385 break;
386 }
387 return NS_OK;
388 }
389
PlaySystemSound(const nsAString & aSoundAlias)390 NS_IMETHODIMP nsSound::PlaySystemSound(const nsAString &aSoundAlias) {
391 if (NS_IsMozAliasSound(aSoundAlias)) {
392 NS_WARNING(
393 "nsISound::playSystemSound is called with \"_moz_\" events, they are "
394 "obsolete, use nsISound::playEventSound instead");
395 uint32_t eventId;
396 if (aSoundAlias.Equals(NS_SYSSOUND_ALERT_DIALOG))
397 eventId = EVENT_ALERT_DIALOG_OPEN;
398 else if (aSoundAlias.Equals(NS_SYSSOUND_CONFIRM_DIALOG))
399 eventId = EVENT_CONFIRM_DIALOG_OPEN;
400 else if (aSoundAlias.Equals(NS_SYSSOUND_MAIL_BEEP))
401 eventId = EVENT_NEW_MAIL_RECEIVED;
402 else if (aSoundAlias.Equals(NS_SYSSOUND_MENU_EXECUTE))
403 eventId = EVENT_MENU_EXECUTE;
404 else if (aSoundAlias.Equals(NS_SYSSOUND_MENU_POPUP))
405 eventId = EVENT_MENU_POPUP;
406 else
407 return NS_OK;
408 return PlayEventSound(eventId);
409 }
410
411 nsresult rv;
412 nsCOMPtr<nsIURI> fileURI;
413
414 // create a nsIFile and then a nsIFileURL from that
415 nsCOMPtr<nsIFile> soundFile;
416 rv = NS_NewLocalFile(aSoundAlias, true, getter_AddRefs(soundFile));
417 NS_ENSURE_SUCCESS(rv, rv);
418
419 rv = NS_NewFileURI(getter_AddRefs(fileURI), soundFile);
420 NS_ENSURE_SUCCESS(rv, rv);
421
422 nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(fileURI, &rv);
423 NS_ENSURE_SUCCESS(rv, rv);
424
425 rv = Play(fileURL);
426
427 return rv;
428 }
429