macos - 在Swift中捕获OSX媒体控制按钮

标签 macos swift media-player

我希望我的应用程序响应F7,F8和F9键盘媒体控制按钮。

我知道这个可爱的库,但无法与Swift结合使用:https://github.com/nevyn/SPMediaKeyTap

最佳答案

我实际上是前几天自己解决了这个问题。我在上面写了一个blog post和一个Gist

我将嵌入博客文章和最终代码,以防博客或Gist消失。 注意:这是一篇很长的文章,详细介绍了如何构造类以及如何在应用程序的委托(delegate)中调用其他方法。如果您只需要完成的产品(MediaApplication类),请移至底部。它位于XML和Info.plist信息上方。

对于初学者,要从媒体密钥获取密钥事件,您需要创建一个扩展NSApplication的类。这很简单

import Cocoa

class MediaApplication: NSApplication {
}

接下来,我们需要覆盖sendEvent()函数
override func sendEvent(event: NSEvent) {
    if (event.type == .SystemDefined && event.subtype.rawValue == 8) {
        let keyCode = ((event.data1 & 0xFFFF0000) >> 16)
        let keyFlags = (event.data1 & 0x0000FFFF)
        // Get the key state. 0xA is KeyDown, OxB is KeyUp
        let keyState = (((keyFlags & 0xFF00) >> 8)) == 0xA
        let keyRepeat = (keyFlags & 0x1)
        mediaKeyEvent(Int32(keyCode), state: keyState, keyRepeat: Bool(keyRepeat))
    }

    super.sendEvent(event)
}

现在,我不假装完全理解这里发生的事情,但是我认为我有一个不错的主意。 NSEvent对象包含几个关键属性:typesubtypedata1data2Typesubtype相当不言自明,但是data1data2非常模糊。由于该代码仅使用data1,因此我们将要研究它。据我所知,data1包含键事件周围的所有数据。这意味着它包含键代码和所有键标志。似乎按键标志包含有关按键状态(按键是否被按下?按键是否被释放?)以及按键是否被按下并重复信号的信息。我还猜测键代码和键标志都占用了data1中包含的数据的一半,而按位操作将这些数据分离为适当的变量。在获得所需的值之后,我们将调用mediaKeyEvent(),稍后我将介绍它。无论将什么事件发送到我们的MediaApplication,我们都希望默认的NSApplication也能够处理所有事件。为此,我们在函数末尾调用super.sendEvent(event)。现在,让我们看一下mediaKeyEvent()
func mediaKeyEvent(key: Int32, state: Bool, keyRepeat: Bool) {
    // Only send events on KeyDown. Without this check, these events will happen twice
    if (state) {
        switch(key) {
        case NX_KEYTYPE_PLAY:
            // Do work
            break
        case NX_KEYTYPE_FAST:
            // Do work
            break
        case NX_KEYTYPE_REWIND:
            // Do work
            break
        default:
            break
        }
    }
}

这是事情开始变得有趣的地方。首先,我们只想检查state为true时正在按下的键,在这种情况下,只要按下该键即可。一旦检查了密钥,我们将查找NX_KEYTYPE_PLAYNX_KEYTYPE_FASTNX_KEYTYPE_REWIND。如果它们的功能不明显,则NX_KEYTYPE_PLAY是播放/暂停键,NX_KEYTYPE_FAST是下一个键,NX_KEYTYPE_REWIND是上一个键。现在,当按下这些键中的任何一个时都什么也没有发生,因此让我们回顾一些可能的逻辑。我们将从一个简单的场景开始。
case NX_KEYTYPE_PLAY:
    print("Play")
    break

使用此代码后,当您的应用程序检测到已按下“播放/暂停”键时,您会看到“播放”已打印到控制台。简单吧?让我们通过调用应用程序的NSApplicationDelegate中的函数来提高效率。首先,我们假设您的NSApplicationDelegate具有一个名为printMessage的函数。我们将不断进行修改,因此请密切注意所做的更改。它们很小,但是更改将影响您从mediaEventKey调用它们的方式。
func printMessage() {
    print("Hello World")
}

这是最简单的情况。调用printMessage()时,您将在控制台中看到“Hello World”。您可以通过在performSelector上访问NSApplicationDelegate上的MediaApplication来调用它。 performSelector接受Selector,这只是NSApplicationDelegate中函数的名称。
case NX_KEYTYPE_PLAY:
    delegate!.performSelector("printMessage")
    break

现在,当您的应用程序检测到已按下“播放/暂停”键时,将在控制台上看到“Hello World”。让我们用一个带有参数的新版本printMessage来提高层次。
func printMessage(arg: String) {
    print(arg)
}

现在的想法是,如果调用printMessage("Hello World"),您将在控制台中看到“Hello World”。现在,我们可以修改performSelector调用以处理传入参数。
case NX_KEYTYPE_PLAY:
    delegate!.performSelector("printMessage:", withObject: "Hello World")
    break

关于此更改,需要注意几件事。首先,注意添加到:中的Selector很重要。当函数名称发送给委托(delegate)时,它将函数名称与参数分开。记住它的工作方式并不是很重要,但这与调用printMessage:"Hello World"的委托(delegate)相似。我相当确定这不是100%正确的,因为它可能会使用某种对象ID,但是我没有做任何深入的研究。无论哪种方式,要记住的重要事情是在传递参数时添加:。要注意的第二件事是我们添加了withObject参数。 withObjectAnyObject?作为值。在这种情况下,我们只是传入String,因为这就是printMessage所要查找的。当您的应用程序检测到已按下播放/暂停键时,您仍应在控制台中看到“Hello World”。让我们看一个最终的用例:printMessage的一个版本,它不接受一个参数,而是两个参数。
func printMessage(arg: String, _ arg2: String) {
    print(arg)
}

现在,如果调用printMessage("Hello", "World"),您将在控制台中看到“Hello World”。现在,我们可以修改performSelector调用以处理传入的两个参数。
case NX_KEYTYPE_PLAY:
    delegate!.performSelector("printMessage::", withObject: "Hello", withObject: "World")
    break

和以前一样,这里有两件事需要注意。首先,我们现在在:的末尾添加两个Selector。像以前一样,这样做是为了使委托(delegate)可以传递包含参数的信息。从最基本的 Angular 来看,它看起来像printMessage:"Hello":"World",但我仍然不知道它在更深层次上的真正样子。注意的第二件事是,我们在withObject调用中添加了第二个performSelector参数。像以前一样,此withObjectAnyObject?作为值,并且我们传递了String,因为这就是printMessage想要的。当您的应用程序检测到已按下播放/暂停键时,您仍应在控制台中看到“Hello World”。

最后要注意的一件事是performSelector最多只能接受两个参数。我真的很想看到Swift添加诸如splatting或varargs之类的概念,以便最终消除此限制,但是现在就避免尝试调用需要两个以上参数的函数。

完成以上所有操作后,这就是一个非常简单的MediaApplication类,它只打印出一些文本:
import Cocoa

class MediaApplication: NSApplication {
    override func sendEvent(event: NSEvent) {
        if (event.type == .SystemDefined && event.subtype.rawValue == 8) {
            let keyCode = ((event.data1 & 0xFFFF0000) >> 16)
            let keyFlags = (event.data1 & 0x0000FFFF)
            // Get the key state. 0xA is KeyDown, OxB is KeyUp
            let keyState = (((keyFlags & 0xFF00) >> 8)) == 0xA
            let keyRepeat = (keyFlags & 0x1)
            mediaKeyEvent(Int32(keyCode), state: keyState, keyRepeat: Bool(keyRepeat))
        }

        super.sendEvent(event)
    }

    func mediaKeyEvent(key: Int32, state: Bool, keyRepeat: Bool) {
        // Only send events on KeyDown. Without this check, these events will happen twice
        if (state) {
            switch(key) {
            case NX_KEYTYPE_PLAY:
                print("Play")
                break
            case NX_KEYTYPE_FAST:
                print("Next")
                break
            case NX_KEYTYPE_REWIND:
                print("Prev")
                break
            default:
                break
            }
        }
    }
}

现在,我还应该补充一点,默认情况下,您的应用程序将在运行时使用标准NSApplication。如果您想使用整个文章所涉及的MediaApplication,则需要继续修改应用程序的Info.plist文件。如果您在图形 View 中,它将看起来像这样:

Info.plist
(来源:sernprogramming.com)

否则,它将看起来像这样:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>CFBundleDevelopmentRegion</key>
  <string>en</string>
  <key>CFBundleExecutable</key>
  <string>$(EXECUTABLE_NAME)</string>
  <key>CFBundleIconFile</key>
  <string></string>
  <key>CFBundleIdentifier</key>
  <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
  <key>CFBundleInfoDictionaryVersion</key>
  <string>6.0</string>
  <key>CFBundleName</key>
  <string>$(PRODUCT_NAME)</string>
  <key>CFBundlePackageType</key>
  <string>APPL</string>
  <key>CFBundleShortVersionString</key>
  <string>1.0</string>
  <key>CFBundleSignature</key>
  <string>????</string>
  <key>CFBundleVersion</key>
  <string>1</string>
  <key>LSApplicationCategoryType</key>
  <string>public.app-category.utilities</string>
  <key>LSMinimumSystemVersion</key>
  <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
  <key>LSUIElement</key>
  <true/>
  <key>NSHumanReadableCopyright</key>
  <string>Copyright © 2015 Chris Rees. All rights reserved.</string>
  <key>NSMainNibFile</key>
  <string>MainMenu</string>
  <key>NSPrincipalClass</key>
  <string>NSApplication</string>
</dict>
</plist>

无论哪种情况,您都需要更改NSPrincipalClass属性。新值将包含您的项目名称,因此将类似于Notify.MediaApplication。进行更改后,运行您的应用程序并使用这些媒体密钥!

关于macos - 在Swift中捕获OSX媒体控制按钮,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32499676/

相关文章:

ios - Init、带参数的 Init 和 Convenience Init

android - 播放一些声音后,媒体播放器无法播放声音

android - MediaPlayer 流媒体错误

Android MediaPlayer 错误 (1, 9100) - Lg L7

objective-c - 如何在 cocoa 中创建启动条目

macos - MAC OSX : Is it possible to create an instance and use a Internet Plugin directly

macos - 制作 CUDA 5.0 示例时遇到 Open MPI 相关问题 (Mac OS X ML)

objective-c - 确定 NSNaturalTextAlignment 的实际文本对齐值?

swift - 让 UITableViewRowAction 在点击时反弹

ios - 从 subview 返回时 Viewdidappear 不会调用