B035c8e46b434111b279f612ac80db97
用 SiriKit 播放你的 App 内容: SiriKit Media Intents

WWDC 2019 Session 207: Introducting SiriKit Media Intent

引言

新的 SiriKit Media Intents 支持用户用自然语言来告诉 Siri 进行播放相关的操作,包括了播放指定歌曲、将歌曲加入收藏、喜欢某首歌曲等等。举个例子,用户说「在 Apple Music 中播放周杰伦的歌曲」,Siri 即可在后台唤起 Apple Music,然后在 Siri 中显示一个播放器,直接开始播放歌曲。这个新功能减少了用户的操作路径,让用户的播放流程更自然,对音频类的 App 有较大的价值(Apple Music 在 iOS 12 上就支持了该功能,大家可以用下面介绍的短语体验)。本文在 Session 内容的基础上加入了作者的一些实践结果。

Apple Music 中对 Media Intent 的支持

Session 207 分为三个部分:

  1. 介绍新的 SiriKit Media Intents
  2. 处理 SiriKit Media Request
  3. 最佳实践

1. 新的 SiriKit Media Intents:

  • INPlayMediaIntent 可以让用户播放一些音频,触发短语是「在 < 我的 App > 中播放 < 歌曲 >」,比如「在 Apple Music 中播放夜曲」。也支持包括倍速播放、随机播放、上一首下一首等功能。
  • INAddMediaIntent 将音频加入播放列表,比如「将这首歌加入我的播放列表」。
  • INUpdateMediaAffinityIntent 用户可以标记音频为喜欢或不喜欢,帮助 App 进行个性化推荐,比如「我喜欢这首歌」。
  • INSearchForMediaIntent 用户可以搜索指定的音频节目,触发短语是「在 < 我的 App > 中搜索 < 歌曲 >」,比如「在 Apple Music 中搜索邓丽君的歌曲」。

支持的5种音频类型

  • 音乐 Music 「在 < 我的 App > 中播放 < 歌曲 >」。支持的类型也包括专辑、歌手、播放列表、歌曲类型等。在 INMediaItemType 中有详细的类型支持列表,详情可以查看文档. 同时,该 Intent 也支持播放控制,如倍速播放、随机播放、上下首等。
  • 播客 Podcasts 「在 < 我的 App > 中播放播客 Stuff You Should Know」。同样支持播放控制。
  • 有声书 Audiobooks 「在 < 我的 App > 中播放有声书 Becoming」。支持倍速播放。
  • 电台 Radio 「在 < 我的 App > 中播放 89.1FM」。
  • 通用类型 General 如果你的 App 不支持以上几种类型的音频,SiriKit 也支持一些通用的识别。「在 < 我的 App > 中播放 < 节目 >」,Siri 依然可以识别出 < 节目 >,并提供该字段的信息给你的 App,只是该信息就不会带有 INMediaSearch 中列举出类型的信息了。

作者注:在上述短语中有一点需要特别注意,如果短语不含有特定的音频类型的词,比如「歌曲」、「播客」、「有声书」,Siri 也无法为识别出来的内容加上类型信息。所以很大程度上需要开发者自己对 Siri 提供的信息进行再次处理,以提供准确的内容。各种语言的 Siri 都会有这种情况出现。期待 Siri 有更充足的语料库来进行准确识别。除此之外,App 的名字也是必须的。用户必须说「在 < App > 中播放」,Siri 才会把 Intent 交给这个 App 处理,否则 Siri 一般会调起 Apple Music。

2. 处理 SiriKit Media Request

处理 SiriKit Media Request 的过程与通常的 SiriKit 处理过程类似,所有的请求处理都在 Intent App Extension 中完成。

当用户说「在我的App中播放一首好歌」时,处理过程就开始了。 Siri 会识别出这个 Intent,并启动你的 Intents App Extention。处理过程共分三步:Resolve、Confirm、Handle。

1. Resolve

在播放的场景中,我们会从参数 intent 中获取 INMediaSearch 对象,我们使用该对象的信息在 App 中进行搜索。获取一个或多个可以用于播放的 INMediaItem。如果找不到或者产生了错误,可以返回 INPlayMediaMediaItemResolutionResult.unsupported(),这会让 Siri 显示一个合适的错误提示。

func resolveMediaItems(for intent: INPlayMediaIntent, with completion: 
([INPlayMediaMediaItemResolutionResult]) -> Void) {
  var result = INPlayMediaMediaItemResolutionResult.unsupported() // 初始化为 unsupported
  if let mediaName = intent.mediaSearch?.mediaName { // 取出 mediaName 用于之后的查找
    for item in mediaItemsFromMyAppCatalog(intent) where item.name == mediaName {
      let mediaItem = INMediaItem(identifier: item.id, title: item.name, type:
        item.type, artwork: nil, artist: item.artist) // 找到了对应的 mediaItem
      result = INPlayMediaMediaItemResolutionResult.success(with: mediaItem) // 用找到的 mediaItem 生成一个 succes 的 result
      break
    }
  }
  completion([result])
}

这里的 .unsupported() 方法是只会简单的返回不支持,如果需要返回详细的原因,可以使用 unsupported(forReason:),reason 为 INPlayMediaMediaItemUnsupportedReason 类型。

public enum INPlayMediaMediaItemUnsupportedReason : Int {
    case loginRequired
    case subscriptionRequired
    case unsupportedMediaType
    case explicitContentSettings
    case cellularDataSettings
}

在不同 Intent 的 Resolve 方法中,虽然参数有点不同,但是过程是相同的。INMediaSearch 对象中包含了用户需要播放的节目的所有信息,我们需要利用这些信息来找到用户希望播放的节目。首先需要从 INMediaSearch 中取得 MediaName,然后从 App 中找到所有可能的 MediaItem,并根据 MediaName 筛选出用户需要的那一个。如果找到 MediaItem,就可以用它来创建一个 Success Result,作为参数传入 completion

2. Confirm

在播放的场景中,不提倡使用 confirm 这一步。在对 Apple 自己 App 的回顾中发现,如果播放时需要用户确认,会降低用户继续进行播放的可能性。

3. Handle

对于 INPlayMediaIntent 来说,这一步比较简单,我们只需返回 .handleInApp,这会使 App 在后台启动。在 App 后台启动过程中,我们播放对应的音频。

func handle(intent: INPlayMediaIntent, completion: (INPlayMediaIntentResponse) -> Void) {
  completion(INPlayMediaIntentResponse(code: .handleInApp, userActivity: nil)) // 传入 handleInApp,告诉 Siri 在后台启动 App
}

AppDelegateapplication:handle:completionHandler: 方法中,取出之前的 MediaItem,开始播放。

func application(_ application: UIApplication, handle intent: INIntent, completionHandler:  (INIntentResponse) -> Void) {
  if let playMediaIntent = intent as? INPlayMediaIntent {
    if let mediaItems = playMediaIntent.mediaItems { // 取出之前放到 Result 中的 MediaItem
      let mediaItemToPlay = mediaItems.first
      // 一些初始化工作,让 App 开始播放节目
      beginPlayback(mediaItemToPlay)
      completionHandler(INPlayMediaIntentResponse(code: .success, userActivity: nil))
    }
  }
}

这里比较有技巧的部分是测试,你需要保证音频正确播放,然而此时你还看不到对应的UI。你也需要确保测试是在正确的场景中进行的,比如在 CarPlay 中。

Demo 演示:

Demo 代码可以从这里下载,这个页面上也包含了详细的配置信息。

如果需要手动添加对新的 Intent 的支持,可以按下面的步骤来。

  1. 将 Intents Extension 加入 App 中:
    前往 File - New - Target,选择 Intent Extension:
    创建 Intent Extension

  2. 在 App 中添加对 Siri 的支持:

  3. 配置支持的 Media Intent:

  4. 确保文件加入 Extension 的编译过程:

  5. 创建 Intent Handler 文件,实现 Resolve 和 Handle 方法:

  6. AppDelegate 中实现 application:handle:completionHandler:

注意点:如果在配置支持的 Media Categories 时没有参考 INMediaItemType 中列出的 Topic 来选,就算说的短语中带上类型词如「歌曲」、「专辑」,Siri 也不会识别出对应的类型。举个例子,如果没有勾选 Music,那么就算说「在 < 我的 App > 中播放歌曲 < 夜曲 >」,INMediaSearch 对象中带着的 INMediaSearchType 也依然是 .unknown 而不是 .song

3. 最佳实践

在支持 shortcuts 的情况下增加对新 Intents 的支持

如果你的 App 在 iOS 12 中就已经支持了 shortcuts,支持新功能将会比较简单。

首先,Handle 方法的实现和在后台启动 App 的代码是相同的,这里无需添加额外代码。
需要实现的是 Resolve 方法。
最后,为你的 App 支持的媒体类型更新 Intents Extension。

代码中需要增加的只有 resolve 方法的实现

对 Apple Watch 的支持

由于 Watch 上的 App 需要在前台启动,因此与在 iOS 上不同的是,需要返回的 INPlayMediaIntentResponseCode.continueInApp,之后的处理会在 WKExtensionDelegate 完成。这部分代码与在 AppDelegate 的中类似,取出 Intent 中的 MediaItem 然后开始播放即可。

不同的是由于网络在 Watch 上是比较珍贵的资源,这里推荐尽量使用缓存来处理,只有在必要的时候才进行网络请求。

在 Resolve 方法中进行高效的查找

当用户说「在 < 我的App > 中播放 < 歌名 >」时,一般我们会用这歌名去进行一些匹配搜索的操作。这里需要考虑一些匹配上的问题:

对英文来说,大小写、重音等是可以忽略的部分,只要单词相同,就应该返回同样的结果。

对中文来说,情况可能复杂一些。如果歌名是 Siri 中未收录的词,可能会产生音同字不同的识别结果,这对直接进行字段匹配不太友好。因此如果 MediaName 中给出的词,我们在进行直接搜索后发现没有匹配结果,可以使用拼音加上音调的形式再进行一次搜索,有利于得到更准确的结果。

由于一些歌曲的曲名会带有版本等额外的信息,用户一般不会在和 Siri 交互时说出这些信息,因此不要依赖于曲名的完全匹配,而需要提取出关键信息。

上图中的「Deluxe Edition」、「Music from the Motion Picture」、「Feat.」 等信息就是一个很好的例子,在进行曲目搜索时不需要完全匹配这些信息。

误划分

专辑或歌曲的名字中带有节目类型的词,如「主播」、「歌曲」、「节目」等词时,Siri 可能会误划分句子。Session 中举了个例子:在「Play The Stuff You Should Know Podcast in < MyApp >」中,其实「The Stuff You Should Know Podcast」是一个完整的播客名字,然而 Siri 却将 Podcast 划分为类型信息,导致名字信息不完整。中文中也有这个问题,需要小心标题中出现「歌曲」等类型词时的情况(比如「歌曲串烧」)。

{
  "mediaName" : "Stuff You Should Know",
  "mediaType" : INMediaItemTypePodcast
}

同音词

由于同音词和书写形式的不同,Siri 给的词可能不会完全满足你的预期。比如按是否缩写会产生「eighty-first」和「81st」两种格式,相同发音的词有「son」与「sun」,中文中也会有「1969」和「一九六九」两种书写形式的数字。所以搜索时最好能有一些灵活性,不完全依赖 Siri 给的词,考虑使用模糊匹配等方法。

对中文而言,音同字不同的情况会更多一些。前面也提到,对 Siri 已经收录的词,识别的结果相对较准确,但对于一些新词,Siri 给的结果就不太令人满意了,考虑使用拼音作为备用的搜索词,有利于得到更好的结果。

优化 Siri 的回答

通过设置 INMediaItem 的属性,可以影响 Siri 说的句子。

尽量在 INMediaItem 中加上 artisttitletype 信息,他们都会影响 Siri 的回答。如果返回的 INMediaItem 有多个,Siri 会使用第一个中的信息。

错误处理

合适的错误处理能让用户明白到底发生了哪些问题。最常见的错误是找不到对应的 MediaItem,这时候需要返回

INPlayMediaMediaItemResolutionResult(INMediaItemResolutionResult.unsupported())

除此之外还有一些错误:如播放某个时需要获取用户信息,然而用户却把信息授权关闭了;或者播放的节目需要用户订阅。

不同的错误信息会触发 Siri 不同的回答。在 INPlayMediaMediaItemUnsupportedReason 中列出了 Siri 支持的错误类型,可以参考前文。

用户可能会对 Siri 说的

播放目标不明确

用户可能说「播放 < 我的App >」,但是没有说要播放的内容。此时如果已经有播放列表,继续播放可能是一个比较合适的选择。如果没有这种列表,可以播放推荐节目,播放一些有趣的歌单等。如果发现 Intent 的 INMediaSearch 对象是 nil,很可能是用户没有指明播放的内容,就可以用上面的策略。

{
  playShuffled = 0;
  mediaSearch = <null>;
  mediaContainer = <null>;
  playbackRepeatMode = none;
  mediaItems = <null>;
  resumePlayback = 0;
  playbackQueueLocation = now;
  playbackSpeed = <null>;
}

上面是直接说「播放 < 我的 App >」时的 Intent,可以看到其中 mediaSearch 对象为空。

不要去问用户播放什么内容,因为用户很可能不再进行下一步。

播放选项

用户说的词可能包含播放选项,如单曲循环、随机播放、以多倍速播放、指定下一首播放内容,因此需要在实现的方法中对这些选项进行处理。

循环播放:「在 < 我的 App > 中循环播放 < 歌曲 >」
随机播放:「在 < 我的 App > 中随机播放 < 播放列表 >」
倍速播放:「在 < 我的 App > 中两倍速继续播放 < 播客 >」
稍后播放:「在 < 我的 App > 中接下来播放 < 歌曲 >」

排序信息

「在 < 我的 App > 中播放 < 播客 > 的最新一集」
「在 < 我的 App > 中播放 < 歌手 > 最好的专辑」
「在 < 我的 App > 中播放好听的歌」

其中,「最新」、「最好」这样的词都会被 Siri 提取为 INMediaSortOrder 放在 INMediaSearch 对象中,支持的排序类型可以参考文档

获取当前正在播的歌曲

当用户说:
「将这首歌加到我的资料库中」
「我喜欢这首歌」
此时 intent.mediaSearch.reference = INMediaReferenceCurrentlyPlaying,代表该 Intent 获取了当前正在播放的媒体。

要获取正在播放的 MediaItem,可以在开始播放时设置 MPNowPlayingInfoCenter.default().nowPlayingInfo,添加 MPNowPlayingInfoPropertyExternalContentIdentifier,设置 MediaItem 的 Identifier 为该 key 的关联对象,即可在后续用户说出喜欢这首歌时,从 intent.mediaSearch.mediaIdentifier 中获取之前设置的 Identifier。

  var nowPlayingInfo: [String: Any] = [ : ]        
  nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = "albumTitle"
  nowPlayingInfo[MPMediaItemPropertyTitle] = "title"
  nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] = "identifier"
  MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo 

「我喜欢这首歌」会调起 resolveAffinityTypeForUpdateMediaAffinity:withCompletion: 方法,其中参数 Intent(类型为 INUpdateMediaAffinityIntent)结构如下

{
    mediaItems = <null>;
    affinityType = like;
    mediaSearch = <INMediaSearch: 0x102b11f20> {
        reference = 1; // INMediaReferenceCurrentlyPlaying
        mediaType = 1; // INMediaItemTypeSong
        sortOrder = 0;
        albumName = 专辑名; //取自 MPMediaItemPropertyAlbumTitle,需要之前设置过
        mediaName = 歌曲名; //取自 MPMediaItemPropertyTitle,需要之前设置过
        genreNames = (
        );
        artistName = <null>;
        moodNames = (
        );
        releaseDate = <null>;
        mediaIdentifier = x-sampmeditem://external/psid; // 取自 MPNowPlayingInfoPropertyExternalContentIdentifier 需要之前设置过
        activityNames = (
        );
    };
}


可以使用 mediaIdentifier 来做收藏等操作。

让 Siri 更了解你的用户

使用 Vocabulary

用户词组能帮助 Siri 识别出重要的媒体。

在 Vocabulary 中,只提交你的 App 用户感兴趣的词组,不需要提交所有的词组。由于这个词组是有序的,所以把对用户更重要的词组放在列表前面。

为这些词设置其类型。可选类型包括 PlaylistTitleMusicArtistNameAudiobookTitleAudiobookAuthorShowTitle。音乐类的 App 使用 PlaylistTitleMusicArtistName 两种类型,有声书类的 App 使用 AudiobookTitleAudiobookAuthor。播客类 App 使用 ShowTitle

详细的设置流程可以参考 Global vocabulary 的文档

总结

本 Session 介绍了四种 Media Intent,可以用于播放指定歌曲、将歌曲加入列表、收藏歌曲、搜索歌曲。

为用户提供最好的 Siri 交互体验,包括搜索的灵活性,错误处理,配置 INMediaItem 来让 Siri 准确说出歌曲信息。

帮助 Siri 了解你的用户,将用户感兴趣的词组提供给 Siri。

参考阅读

WWDC 2017 Session 228:Making Great SiriKit Experiences
WWDC 2016 Session 217:Introducing SiriKit

© 著作权归作者所有
这个作品真棒,我要支持一下!
一年一度的 WWDC 又来啦!今年国内三大 iOS 组织(排名不分先后): 老司机 iOS 周报 知识小集 Sw...
18条评论

Siri现在越来越厉害了,但是我个人使用的最大问题是,Siri的响应速度太慢了。

Alria
#2

关于处理INSearchForMediaIntent这一类型作者有试过吗,在extension的handle中设置为"continueInApp"时,好像是直接唤起主app,没有走appdelegate中的handleIntent的回调方法

wiilen
#3

#2楼 @Alria 是的,ContinueInApp 的意思是打开 App 继续执行,INSearchForMediaIntent 也只支持这种处理方式。你可以看到 INSearchForMediaIntentResponseCode 也并没有 HandleInApp 这个 Code。你可以在 INSearchForMediaIntentResponse 创建时加上 userActivity,把需要的参数带上,然后在 AppDeledate 中的 application(_:continue:restorationHandler:) 进行处理。

Alria
#4

#3楼 @wiilen 嗯我在官方文档有看到过类似的说明,如你所描述的那样操作,在IntentResponse初始化时加上userActivity,但在唤起主app时却并没有调用AppDeledate 中的 application(_:continue:restorationHandler:) ,同时在Xcode控制台会输出这样一句打印
[Intents] -[INCache cacheableObjectForIdentifier:] Unable to find cacheable object with identifier E84E3A83-3C53-4146-A47D-5172B1F7FFA2 in cache.
这个是官方的问题还是我哪里漏配置了吗。

wiilen
#5

#4楼 @Alria handleSearchForMedia:completion:有被调到吗?配置的话,Target->General->Support Intents 中需要手动加上 INSearchForMediaIntent。你也可以附上你的代码。

Alria
#6

#5楼 @wiilen handleSearchForMedia有调用到,Support Intents也有填下。包括INPlayMediaIntent,用ContinueInApp的回应方式也是没有触发application(_:continue:restorationHandler:),用HandleInApp的方式倒是可以在application:handle:completionHandler:中响应处理。但问题是INSearchForMediaIntent只有ContinueInApp,没有HandleInApp😿
handleSearchForMedia里的实现,我的测试代码是这样:

    NSUserActivity *activity = [[NSUserActivity alloc] initWithActivityType:NSStringFromClass([INSearchForMediaIntent class])];
    activity.userInfo ="songName": @"MerryChristmas"};
    INSearchForMediaIntentResponse *res = [[INSearchForMediaIntentResponse alloc] initWithCode:(INSearchForMediaIntentResponseCodeContinueInApp) userActivity:activity];
    completion(res);

对siri说:在<appname>搜索<xxx>确实调用了到了这里,也唤起了主app,但application(_:continue:restorationHandler:)并没有调用..

wiilen
#7

#6楼 @Alria 你是单独编译了 Siri Intent 吗?那样的话需要在 attach to process 中连上你的主 App,断点才会进到 application(_:continue:restorationHandler:) 的。不知道是否是这个原因...

Cocoa65
#9

请教下,对siri说:在<appname>搜索<xxx>,这个appname在product name和display name不一致时是哪一个呢?我尝试用display name,siri告诉我找不到app,使用product name时倒可以调用倒app中的resolve的方法。

wiilen
#10

#9楼 @Cocoa65 这个我也没有尝试过,应该是和你的结果一致吧,你也可以用这篇文档中的方法提供一个名字 https://developer.apple.com/documentation/sirikit/registering_custom_vocabulary_with_sirikit/specifying_synonyms_for_your_app_name

Cocoa65
#11

#10楼 @wiilen 谢谢

Alria
#12

#8楼 @wiilen 是有唤醒,但是没走application(_:continue:restorationHandler:) 方法。跟断点应该没关系.方法里写的相关逻辑都没执行。。您那边有尝试过searchMediaIntent吗?

wiilen
#13

#12楼 @Alria 在公司项目里有的,我这里也没做其他处理,只是加了 SearchMediaIntent 的 Handle 方法,加了 userActivity,然后在 application(_:continue:restorationHandler:) 里打开了搜索页,所以我也不太确定你的代码是哪部分没有配置好还是什么问题

Alria
#14

#13楼 @wiilen 🤦‍♂️难道会是Xcode或iOS版本的问题?我这边再试下新建一个项目,除了在info.plist中支持searchMedia,实现协议里的handle方法选择ContinueInApp方式回应之外,不加其它多余代码,看能不能获取到userActivity

Alria
#15

#13楼 @wiilen 还是不行🤦‍♀️绝望了,官方文档都翻烂了,Handle能触发但就是application(_:continue:restorationHandler:)不触发

Alria
#16

#13楼 @wiilen 加userActivity的时候activityType属性会需要特别处理吗?还是随意定一个字符串的

wiilen
#17

#15楼 @Alria 我看看啥时候新起一个项目试试吧...我用的是「appName.siri.search.media」,好像没有特别的影响,和其他的不一样就行了

Alria
#18

#17楼 @wiilen 谢谢大佬...🤦‍♂️我看官方文档的示例的activityType也是随意取的,应该不用特殊处理

top Created with Sketch.