/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "MPRISServiceHandler.h" #include #include #include #include "MPRISInterfaceDescription.h" #include "mozilla/dom/MediaControlUtils.h" #include "mozilla/GRefPtr.h" #include "mozilla/GUniquePtr.h" #include "mozilla/UniquePtrExtensions.h" #include "mozilla/Maybe.h" #include "mozilla/ScopeExit.h" #include "mozilla/Sprintf.h" #include "nsXULAppAPI.h" #include "nsIXULAppInfo.h" #include "nsIOutputStream.h" #include "nsNetUtil.h" #include "nsServiceManagerUtils.h" #include "WidgetUtilsGtk.h" #include "prio.h" #define LOGMPRIS(msg, ...) \ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \ ("MPRISServiceHandler=%p, " msg, this, ##__VA_ARGS__)) namespace mozilla { namespace widget { // A global counter tracking the total images saved in the system and it will be // used to form a unique image file name. static uint32_t gImageNumber = 0; static inline Maybe GetMediaControlKey( const gchar* aMethodName) { const std::unordered_map map = { {"Raise", dom::MediaControlKey::Focus}, {"Next", dom::MediaControlKey::Nexttrack}, {"Previous", dom::MediaControlKey::Previoustrack}, {"Pause", dom::MediaControlKey::Pause}, {"PlayPause", dom::MediaControlKey::Playpause}, {"Stop", dom::MediaControlKey::Stop}, {"Play", dom::MediaControlKey::Play}}; auto it = map.find(aMethodName); return it == map.end() ? Nothing() : Some(it->second); } static void HandleMethodCall(GDBusConnection* aConnection, const gchar* aSender, const gchar* aObjectPath, const gchar* aInterfaceName, const gchar* aMethodName, GVariant* aParameters, GDBusMethodInvocation* aInvocation, gpointer aUserData) { MOZ_ASSERT(aUserData); MOZ_ASSERT(NS_IsMainThread()); Maybe key = GetMediaControlKey(aMethodName); if (key.isNothing()) { g_dbus_method_invocation_return_error( aInvocation, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "Method %s.%s.%s not supported", aObjectPath, aInterfaceName, aMethodName); return; } MPRISServiceHandler* handler = static_cast(aUserData); if (handler->PressKey(key.value())) { g_dbus_method_invocation_return_value(aInvocation, nullptr); } else { g_dbus_method_invocation_return_error( aInvocation, G_DBUS_ERROR, G_DBUS_ERROR_FAILED, "%s.%s.%s is not available now", aObjectPath, aInterfaceName, aMethodName); } } enum class Property : uint8_t { eIdentity, eDesktopEntry, eHasTrackList, eCanRaise, eCanQuit, eSupportedUriSchemes, eSupportedMimeTypes, eCanGoNext, eCanGoPrevious, eCanPlay, eCanPause, eCanSeek, eCanControl, eGetPlaybackStatus, eGetMetadata, }; static inline Maybe GetPairedKey(Property aProperty) { switch (aProperty) { case Property::eCanRaise: return Some(dom::MediaControlKey::Focus); case Property::eCanGoNext: return Some(dom::MediaControlKey::Nexttrack); case Property::eCanGoPrevious: return Some(dom::MediaControlKey::Previoustrack); case Property::eCanPlay: return Some(dom::MediaControlKey::Play); case Property::eCanPause: return Some(dom::MediaControlKey::Pause); default: return Nothing(); } } static inline Maybe GetProperty(const gchar* aPropertyName) { const std::unordered_map map = { // org.mpris.MediaPlayer2 properties {"Identity", Property::eIdentity}, {"DesktopEntry", Property::eDesktopEntry}, {"HasTrackList", Property::eHasTrackList}, {"CanRaise", Property::eCanRaise}, {"CanQuit", Property::eCanQuit}, {"SupportedUriSchemes", Property::eSupportedUriSchemes}, {"SupportedMimeTypes", Property::eSupportedMimeTypes}, // org.mpris.MediaPlayer2.Player properties {"CanGoNext", Property::eCanGoNext}, {"CanGoPrevious", Property::eCanGoPrevious}, {"CanPlay", Property::eCanPlay}, {"CanPause", Property::eCanPause}, {"CanSeek", Property::eCanSeek}, {"CanControl", Property::eCanControl}, {"PlaybackStatus", Property::eGetPlaybackStatus}, {"Metadata", Property::eGetMetadata}}; auto it = map.find(aPropertyName); return (it == map.end() ? Nothing() : Some(it->second)); } static GVariant* HandleGetProperty(GDBusConnection* aConnection, const gchar* aSender, const gchar* aObjectPath, const gchar* aInterfaceName, const gchar* aPropertyName, GError** aError, gpointer aUserData) { MOZ_ASSERT(aUserData); MOZ_ASSERT(NS_IsMainThread()); Maybe property = GetProperty(aPropertyName); if (property.isNothing()) { g_set_error(aError, G_DBUS_ERROR, G_DBUS_ERROR_NOT_SUPPORTED, "%s.%s %s is not supported", aObjectPath, aInterfaceName, aPropertyName); return nullptr; } MPRISServiceHandler* handler = static_cast(aUserData); switch (property.value()) { case Property::eSupportedUriSchemes: case Property::eSupportedMimeTypes: // No plan to implement OpenUri for now return g_variant_new_strv(nullptr, 0); case Property::eGetPlaybackStatus: return handler->GetPlaybackStatus(); case Property::eGetMetadata: return handler->GetMetadataAsGVariant(); case Property::eIdentity: return g_variant_new_string(handler->Identity()); case Property::eDesktopEntry: return g_variant_new_string(handler->DesktopEntry()); case Property::eHasTrackList: case Property::eCanQuit: case Property::eCanSeek: return g_variant_new_boolean(false); // Play/Pause would be blocked if CanControl is false case Property::eCanControl: return g_variant_new_boolean(true); case Property::eCanRaise: case Property::eCanGoNext: case Property::eCanGoPrevious: case Property::eCanPlay: case Property::eCanPause: Maybe key = GetPairedKey(property.value()); MOZ_ASSERT(key.isSome()); return g_variant_new_boolean(handler->IsMediaKeySupported(key.value())); } MOZ_ASSERT_UNREACHABLE("Switch statement is incomplete"); return nullptr; } static gboolean HandleSetProperty(GDBusConnection* aConnection, const gchar* aSender, const gchar* aObjectPath, const gchar* aInterfaceName, const gchar* aPropertyName, GVariant* aValue, GError** aError, gpointer aUserData) { MOZ_ASSERT(aUserData); MOZ_ASSERT(NS_IsMainThread()); g_set_error(aError, G_IO_ERROR, G_IO_ERROR_FAILED, "%s:%s setting is not supported", aInterfaceName, aPropertyName); return false; } static const GDBusInterfaceVTable gInterfaceVTable = { HandleMethodCall, HandleGetProperty, HandleSetProperty}; void MPRISServiceHandler::OnNameAcquiredStatic(GDBusConnection* aConnection, const gchar* aName, gpointer aUserData) { MOZ_ASSERT(aUserData); static_cast(aUserData)->OnNameAcquired(aConnection, aName); } void MPRISServiceHandler::OnNameLostStatic(GDBusConnection* aConnection, const gchar* aName, gpointer aUserData) { MOZ_ASSERT(aUserData); static_cast(aUserData)->OnNameLost(aConnection, aName); } void MPRISServiceHandler::OnBusAcquiredStatic(GDBusConnection* aConnection, const gchar* aName, gpointer aUserData) { MOZ_ASSERT(aUserData); static_cast(aUserData)->OnBusAcquired(aConnection, aName); } void MPRISServiceHandler::OnNameAcquired(GDBusConnection* aConnection, const gchar* aName) { LOGMPRIS("OnNameAcquired: %s", aName); mConnection = aConnection; } void MPRISServiceHandler::OnNameLost(GDBusConnection* aConnection, const gchar* aName) { LOGMPRIS("OnNameLost: %s", aName); mConnection = nullptr; if (!mRootRegistrationId) { return; } if (g_dbus_connection_unregister_object(aConnection, mRootRegistrationId)) { mRootRegistrationId = 0; } else { // Note: Most code examples in the internet probably dont't even check the // result here, but // according to the spec it _can_ return false. LOGMPRIS("Unable to unregister root object from within onNameLost!"); } if (!mPlayerRegistrationId) { return; } if (g_dbus_connection_unregister_object(aConnection, mPlayerRegistrationId)) { mPlayerRegistrationId = 0; } else { // Note: Most code examples in the internet probably dont't even check the // result here, but // according to the spec it _can_ return false. LOGMPRIS("Unable to unregister object from within onNameLost!"); } } void MPRISServiceHandler::OnBusAcquired(GDBusConnection* aConnection, const gchar* aName) { GUniquePtr error; LOGMPRIS("OnBusAcquired: %s", aName); mRootRegistrationId = g_dbus_connection_register_object( aConnection, DBUS_MPRIS_OBJECT_PATH, mIntrospectionData->interfaces[0], &gInterfaceVTable, this, /* user_data */ nullptr, /* user_data_free_func */ getter_Transfers(error)); /* GError** */ if (mRootRegistrationId == 0) { LOGMPRIS("Failed at root registration: %s", error ? error->message : "Unknown Error"); return; } mPlayerRegistrationId = g_dbus_connection_register_object( aConnection, DBUS_MPRIS_OBJECT_PATH, mIntrospectionData->interfaces[1], &gInterfaceVTable, this, /* user_data */ nullptr, /* user_data_free_func */ getter_Transfers(error)); /* GError** */ if (mPlayerRegistrationId == 0) { LOGMPRIS("Failed at object registration: %s", error ? error->message : "Unknown Error"); } } bool MPRISServiceHandler::Open() { MOZ_ASSERT(!mInitialized); MOZ_ASSERT(NS_IsMainThread()); GUniquePtr error; gchar serviceName[256]; InitIdentity(); SprintfLiteral(serviceName, DBUS_MPRIS_SERVICE_NAME ".instance%d", getpid()); mOwnerId = g_bus_own_name(G_BUS_TYPE_SESSION, serviceName, // Enter a waiting queue until this service name is free // (likely another FF instance is running/has been crashed) G_BUS_NAME_OWNER_FLAGS_NONE, OnBusAcquiredStatic, OnNameAcquiredStatic, OnNameLostStatic, this, nullptr); /* parse introspection data */ mIntrospectionData = dont_AddRef( g_dbus_node_info_new_for_xml(introspection_xml, getter_Transfers(error))); if (!mIntrospectionData) { LOGMPRIS("Failed at parsing XML Interface definition: %s", error ? error->message : "Unknown Error"); return false; } mInitialized = true; return true; } MPRISServiceHandler::MPRISServiceHandler() = default; MPRISServiceHandler::~MPRISServiceHandler() { MOZ_ASSERT(!mInitialized, "Close hasn't been called!"); } void MPRISServiceHandler::Close() { gchar serviceName[256]; SprintfLiteral(serviceName, DBUS_MPRIS_SERVICE_NAME ".instance%d", getpid()); // Reset playback state and metadata before disconnect from dbus. SetPlaybackState(dom::MediaSessionPlaybackState::None); ClearMetadata(); OnNameLost(mConnection, serviceName); if (mOwnerId != 0) { g_bus_unown_name(mOwnerId); } mIntrospectionData = nullptr; mInitialized = false; MediaControlKeySource::Close(); } bool MPRISServiceHandler::IsOpened() const { return mInitialized; } void MPRISServiceHandler::InitIdentity() { nsresult rv; nsCOMPtr appInfo = do_GetService("@mozilla.org/xre/app-info;1", &rv); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = appInfo->GetVendor(mIdentity); MOZ_ASSERT(NS_SUCCEEDED(rv)); rv = appInfo->GetName(mDesktopEntry); MOZ_ASSERT(NS_SUCCEEDED(rv)); mIdentity.Append(' '); mIdentity.Append(mDesktopEntry); // Compute the desktop entry name like nsAppRunner does for g_set_prgname ToLowerCase(mDesktopEntry); } const char* MPRISServiceHandler::Identity() const { NS_WARNING_ASSERTION(mInitialized, "MPRISServiceHandler should have been initialized."); return mIdentity.get(); } const char* MPRISServiceHandler::DesktopEntry() const { NS_WARNING_ASSERTION(mInitialized, "MPRISServiceHandler should have been initialized."); return mDesktopEntry.get(); } bool MPRISServiceHandler::PressKey(dom::MediaControlKey aKey) const { MOZ_ASSERT(mInitialized); if (!IsMediaKeySupported(aKey)) { LOGMPRIS("%s is not supported", ToMediaControlKeyStr(aKey)); return false; } LOGMPRIS("Press %s", ToMediaControlKeyStr(aKey)); EmitEvent(aKey); return true; } void MPRISServiceHandler::SetPlaybackState( dom::MediaSessionPlaybackState aState) { LOGMPRIS("SetPlaybackState"); if (mPlaybackState == aState) { return; } MediaControlKeySource::SetPlaybackState(aState); GVariant* state = GetPlaybackStatus(); GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&builder, "{sv}", "PlaybackStatus", state); GVariant* parameters = g_variant_new( "(sa{sv}as)", DBUS_MPRIS_PLAYER_INTERFACE, &builder, nullptr); LOGMPRIS("Emitting MPRIS property changes for 'PlaybackStatus'"); Unused << EmitPropertiesChangedSignal(parameters); } GVariant* MPRISServiceHandler::GetPlaybackStatus() const { switch (GetPlaybackState()) { case dom::MediaSessionPlaybackState::Playing: return g_variant_new_string("Playing"); case dom::MediaSessionPlaybackState::Paused: return g_variant_new_string("Paused"); case dom::MediaSessionPlaybackState::None: return g_variant_new_string("Stopped"); default: MOZ_ASSERT_UNREACHABLE("Invalid Playback State"); return nullptr; } } void MPRISServiceHandler::SetMediaMetadata( const dom::MediaMetadataBase& aMetadata) { // Reset the index of the next available image to be fetched in the artwork, // before checking the fetching process should be started or not. The image // fetching process could be skipped if the image being fetching currently is // in the artwork. If the current image fetching fails, the next availabe // candidate should be the first image in the latest artwork mNextImageIndex = 0; // No need to fetch a MPRIS image if // 1) MPRIS image is being fetched, and the one in fetching is in the artwork // 2) MPRIS image is not being fetched, and the one in use is in the artwork if (!mFetchingUrl.IsEmpty()) { if (dom::IsImageIn(aMetadata.mArtwork, mFetchingUrl)) { LOGMPRIS( "No need to load MPRIS image. The one being processed is in the " "artwork"); // Set MPRIS without the image first. The image will be loaded to MPRIS // asynchronously once it's fetched and saved into a local file SetMediaMetadataInternal(aMetadata); return; } } else if (!mCurrentImageUrl.IsEmpty()) { if (dom::IsImageIn(aMetadata.mArtwork, mCurrentImageUrl)) { LOGMPRIS("No need to load MPRIS image. The one in use is in the artwork"); SetMediaMetadataInternal(aMetadata, false); return; } } // Set MPRIS without the image first then load the image to MPRIS // asynchronously SetMediaMetadataInternal(aMetadata); LoadImageAtIndex(mNextImageIndex++); } bool MPRISServiceHandler::EmitMetadataChanged() const { GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&builder, "{sv}", "Metadata", GetMetadataAsGVariant()); GVariant* parameters = g_variant_new( "(sa{sv}as)", DBUS_MPRIS_PLAYER_INTERFACE, &builder, nullptr); LOGMPRIS("Emit MPRIS property changes for 'Metadata'"); return EmitPropertiesChangedSignal(parameters); } void MPRISServiceHandler::SetMediaMetadataInternal( const dom::MediaMetadataBase& aMetadata, bool aClearArtUrl) { mMPRISMetadata.UpdateFromMetadataBase(aMetadata); if (aClearArtUrl) { mMPRISMetadata.mArtUrl.Truncate(); } EmitMetadataChanged(); } void MPRISServiceHandler::ClearMetadata() { mMPRISMetadata.Clear(); mImageFetchRequest.DisconnectIfExists(); RemoveAllLocalImages(); mCurrentImageUrl.Truncate(); mFetchingUrl.Truncate(); mNextImageIndex = 0; mSupportedKeys = 0; EmitMetadataChanged(); } void MPRISServiceHandler::LoadImageAtIndex(const size_t aIndex) { MOZ_ASSERT(NS_IsMainThread()); if (aIndex >= mMPRISMetadata.mArtwork.Length()) { LOGMPRIS("Stop loading image to MPRIS. No available image"); mImageFetchRequest.DisconnectIfExists(); return; } const dom::MediaImage& image = mMPRISMetadata.mArtwork[aIndex]; if (!dom::IsValidImageUrl(image.mSrc)) { LOGMPRIS("Skip the image with invalid URL. Try next image"); LoadImageAtIndex(mNextImageIndex++); return; } mImageFetchRequest.DisconnectIfExists(); mFetchingUrl = image.mSrc; mImageFetcher = MakeUnique(image); RefPtr self = this; mImageFetcher->FetchImage() ->Then( AbstractThread::MainThread(), __func__, [this, self](const nsCOMPtr& aImage) { LOGMPRIS("The image is fetched successfully"); mImageFetchRequest.Complete(); uint32_t size = 0; char* data = nullptr; // Only used to hold the image data nsCOMPtr inputStream; nsresult rv = dom::GetEncodedImageBuffer( aImage, mMimeType, getter_AddRefs(inputStream), &size, &data); if (NS_FAILED(rv) || !inputStream || size == 0 || !data) { LOGMPRIS("Failed to get the image buffer info. Try next image"); LoadImageAtIndex(mNextImageIndex++); return; } if (SetImageToDisplay(data, size)) { mCurrentImageUrl = mFetchingUrl; LOGMPRIS("The MPRIS image is updated to the image from: %s", NS_ConvertUTF16toUTF8(mCurrentImageUrl).get()); } else { LOGMPRIS("Failed to set image to MPRIS"); mCurrentImageUrl.Truncate(); } mFetchingUrl.Truncate(); }, [this, self](bool) { LOGMPRIS("Failed to fetch image. Try next image"); mImageFetchRequest.Complete(); mFetchingUrl.Truncate(); LoadImageAtIndex(mNextImageIndex++); }) ->Track(mImageFetchRequest); } bool MPRISServiceHandler::SetImageToDisplay(const char* aImageData, uint32_t aDataSize) { if (!RenewLocalImageFile(aImageData, aDataSize)) { return false; } MOZ_ASSERT(mLocalImageFile); mMPRISMetadata.mArtUrl = nsCString("file://"); mMPRISMetadata.mArtUrl.Append(mLocalImageFile->NativePath()); LOGMPRIS("The image file is created at %s", mMPRISMetadata.mArtUrl.get()); return EmitMetadataChanged(); } bool MPRISServiceHandler::RenewLocalImageFile(const char* aImageData, uint32_t aDataSize) { MOZ_ASSERT(aImageData); MOZ_ASSERT(aDataSize != 0); if (!InitLocalImageFile()) { LOGMPRIS("Failed to create a new image"); return false; } MOZ_ASSERT(mLocalImageFile); nsCOMPtr out; NS_NewLocalFileOutputStream(getter_AddRefs(out), mLocalImageFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE); uint32_t written; nsresult rv = out->Write(aImageData, aDataSize, &written); if (NS_FAILED(rv) || written != aDataSize) { LOGMPRIS("Failed to write an image file"); RemoveAllLocalImages(); return false; } return true; } static const char* GetImageFileExtension(const char* aMimeType) { MOZ_ASSERT(strcmp(aMimeType, IMAGE_PNG) == 0); return "png"; } bool MPRISServiceHandler::InitLocalImageFile() { RemoveAllLocalImages(); if (!InitLocalImageFolder()) { return false; } MOZ_ASSERT(mLocalImageFolder); MOZ_ASSERT(!mLocalImageFile); nsresult rv = mLocalImageFolder->Clone(getter_AddRefs(mLocalImageFile)); if (NS_FAILED(rv)) { LOGMPRIS("Failed to get the image folder"); return false; } auto cleanup = MakeScopeExit([this, self = RefPtr(this)] { mLocalImageFile = nullptr; }); // Create an unique file name to work around the file caching mechanism in the // Ubuntu. Once the image X specified by the filename Y is used in Ubuntu's // MPRIS, this pair will be cached. As long as the filename is same, even the // file content specified by Y is changed to Z, the image will stay unchanged. // The image shown in the Ubuntu's notification is still X instead of Z. // Changing the filename constantly works around this problem char filename[64]; SprintfLiteral(filename, "%d_%d.%s", getpid(), gImageNumber++, GetImageFileExtension(mMimeType.get())); rv = mLocalImageFile->Append(NS_ConvertUTF8toUTF16(filename)); if (NS_FAILED(rv)) { LOGMPRIS("Failed to create an image filename"); return false; } rv = mLocalImageFile->Create(nsIFile::NORMAL_FILE_TYPE, 0600); if (NS_FAILED(rv)) { LOGMPRIS("Failed to create an image file"); return false; } cleanup.release(); return true; } bool MPRISServiceHandler::InitLocalImageFolder() { if (mLocalImageFolder && LocalImageFolderExists()) { return true; } nsresult rv = NS_ERROR_FAILURE; if (IsRunningUnderFlatpak()) { // The XDG_DATA_HOME points to the same location in the host and guest // filesystem. if (const auto* xdgDataHome = g_getenv("XDG_DATA_HOME")) { rv = NS_NewNativeLocalFile(nsDependentCString(xdgDataHome), true, getter_AddRefs(mLocalImageFolder)); } } else { rv = NS_GetSpecialDirectory(XRE_USER_APP_DATA_DIR, getter_AddRefs(mLocalImageFolder)); } if (NS_FAILED(rv) || !mLocalImageFolder) { LOGMPRIS("Failed to get the image folder"); return false; } auto cleanup = MakeScopeExit([&] { mLocalImageFolder = nullptr; }); rv = mLocalImageFolder->Append(u"firefox-mpris"_ns); if (NS_FAILED(rv)) { LOGMPRIS("Failed to name an image folder"); return false; } if (!LocalImageFolderExists()) { rv = mLocalImageFolder->Create(nsIFile::DIRECTORY_TYPE, 0700); if (NS_FAILED(rv)) { LOGMPRIS("Failed to create an image folder"); return false; } } cleanup.release(); return true; } void MPRISServiceHandler::RemoveAllLocalImages() { if (!mLocalImageFolder || !LocalImageFolderExists()) { return; } nsresult rv = mLocalImageFolder->Remove(/* aRecursive */ true); if (NS_FAILED(rv)) { // It's ok to fail. The next removal is called when updating the // media-session image, or closing the MPRIS. LOGMPRIS("Failed to remove images"); } LOGMPRIS("Abandon %s", mLocalImageFile ? mLocalImageFile->NativePath().get() : "nothing"); mMPRISMetadata.mArtUrl.Truncate(); mLocalImageFile = nullptr; mLocalImageFolder = nullptr; } bool MPRISServiceHandler::LocalImageFolderExists() { MOZ_ASSERT(mLocalImageFolder); bool exists; nsresult rv = mLocalImageFolder->Exists(&exists); return NS_SUCCEEDED(rv) && exists; } GVariant* MPRISServiceHandler::GetMetadataAsGVariant() const { GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&builder, "{sv}", "mpris:trackid", g_variant_new("o", DBUS_MPRIS_TRACK_PATH)); g_variant_builder_add( &builder, "{sv}", "xesam:title", g_variant_new_string(static_cast( NS_ConvertUTF16toUTF8(mMPRISMetadata.mTitle).get()))); g_variant_builder_add( &builder, "{sv}", "xesam:album", g_variant_new_string(static_cast( NS_ConvertUTF16toUTF8(mMPRISMetadata.mAlbum).get()))); GVariantBuilder artistBuilder; g_variant_builder_init(&artistBuilder, G_VARIANT_TYPE("as")); g_variant_builder_add( &artistBuilder, "s", static_cast( NS_ConvertUTF16toUTF8(mMPRISMetadata.mArtist).get())); g_variant_builder_add(&builder, "{sv}", "xesam:artist", g_variant_builder_end(&artistBuilder)); if (!mMPRISMetadata.mArtUrl.IsEmpty()) { g_variant_builder_add(&builder, "{sv}", "mpris:artUrl", g_variant_new_string(static_cast( mMPRISMetadata.mArtUrl.get()))); } return g_variant_builder_end(&builder); } void MPRISServiceHandler::EmitEvent(dom::MediaControlKey aKey) const { for (const auto& listener : mListeners) { listener->OnActionPerformed(dom::MediaControlAction(aKey)); } } struct InterfaceProperty { const char* interface; const char* property; }; static const std::unordered_map gKeyProperty = { {dom::MediaControlKey::Focus, {DBUS_MPRIS_INTERFACE, "CanRaise"}}, {dom::MediaControlKey::Nexttrack, {DBUS_MPRIS_PLAYER_INTERFACE, "CanGoNext"}}, {dom::MediaControlKey::Previoustrack, {DBUS_MPRIS_PLAYER_INTERFACE, "CanGoPrevious"}}, {dom::MediaControlKey::Play, {DBUS_MPRIS_PLAYER_INTERFACE, "CanPlay"}}, {dom::MediaControlKey::Pause, {DBUS_MPRIS_PLAYER_INTERFACE, "CanPause"}}}; void MPRISServiceHandler::SetSupportedMediaKeys( const MediaKeysArray& aSupportedKeys) { uint32_t supportedKeys = 0; for (const dom::MediaControlKey& key : aSupportedKeys) { supportedKeys |= GetMediaKeyMask(key); } if (mSupportedKeys == supportedKeys) { LOGMPRIS("Supported keys stay the same"); return; } uint32_t oldSupportedKeys = mSupportedKeys; mSupportedKeys = supportedKeys; // Emit related property changes for (auto it : gKeyProperty) { bool keyWasSupported = oldSupportedKeys & GetMediaKeyMask(it.first); bool keyIsSupported = mSupportedKeys & GetMediaKeyMask(it.first); if (keyWasSupported != keyIsSupported) { LOGMPRIS("Emit PropertiesChanged signal: %s.%s=%s", it.second.interface, it.second.property, keyIsSupported ? "true" : "false"); EmitSupportedKeyChanged(it.first, keyIsSupported); } } } bool MPRISServiceHandler::IsMediaKeySupported(dom::MediaControlKey aKey) const { return mSupportedKeys & GetMediaKeyMask(aKey); } bool MPRISServiceHandler::EmitSupportedKeyChanged(dom::MediaControlKey aKey, bool aSupported) const { auto it = gKeyProperty.find(aKey); if (it == gKeyProperty.end()) { LOGMPRIS("No property for %s", ToMediaControlKeyStr(aKey)); return false; } GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}")); g_variant_builder_add(&builder, "{sv}", static_cast(it->second.property), g_variant_new_boolean(aSupported)); GVariant* parameters = g_variant_new( "(sa{sv}as)", static_cast(it->second.interface), &builder, nullptr); LOGMPRIS("Emit MPRIS property changes for '%s.%s'", it->second.interface, it->second.property); return EmitPropertiesChangedSignal(parameters); } bool MPRISServiceHandler::EmitPropertiesChangedSignal( GVariant* aParameters) const { if (!mConnection) { LOGMPRIS("No D-Bus Connection. Cannot emit properties changed signal"); return false; } GError* error = nullptr; if (!g_dbus_connection_emit_signal( mConnection, nullptr, DBUS_MPRIS_OBJECT_PATH, "org.freedesktop.DBus.Properties", "PropertiesChanged", aParameters, &error)) { LOGMPRIS("Failed to emit MPRIS property changes: %s", error ? error->message : "Unknown Error"); if (error) { g_error_free(error); } return false; } return true; } #undef LOGMPRIS } // namespace widget } // namespace mozilla