Enable rich media interaction signals in an iOS app
The rich media interaction signal captures user interactions with video and audio content in your app. In Connect the signal enables segmentation by media ID, media name and media category. Use it to retarget users who watched a product demo but did not add to cart or to reach users who watched a how-to but did not complete the associated action.
Availability: Premium and Ultimate
Languages: Swift and Objective-C
Implementation considerations
Interaction types
The signal supports the following interaction types for the interactionType field:
| Value | When to fire |
|---|---|
LOAD | Media begins loading |
LAUNCH | User initiates playback for the first time |
PAUSE | User pauses playback |
CONTINUE | User resumes playback after pausing |
COMPLETE | User reaches the end of the media |
STOP | User stops playback before completion |
ENLARGE | User enters fullscreen or expands the player |
Not all interaction types are required - instrument the ones relevant to your use case.
Event timing
AVPlayer fires timeControlStatus KVO callbacks and AVPlayerItem notifications on state changes and some transitions can occur multiple times during a single user interaction. Decide whether to track every state change or only significant milestones such as LAUNCH and COMPLETE. A launched guard flag (as shown in the complete example) prevents duplicate LAUNCH signals.
Contact mapping
Include the audience field to attribute this signal to a contact in Connect. See How behavior signals update Connect data for identifier formats and contact resolution details.
Configuration
Before adding behavior signals, integrate the Connect SDK into your app. See the guide for your development language: Swift or Objective-C.
Method
func logSignal(_ data: [String: Any]?) -> BoolSends the signal to the Acoustic Connect endpoint. Initialize the Connect SDK before calling this method using ConnectSDK.shared.enable(with:).
Pass all fields as a flat [String: Any] dictionary. The SDK handles the signal structure automatically.
- (BOOL)logSignal:(NSDictionary *)values;Sends the signal to the Acoustic Connect endpoint. Initialize the Connect SDK before calling this method using [[ConnectApplicationHelper sharedInstance] enableFramework:withPostMessageUrl:].
Pass all fields as an NSDictionary. The SDK handles the signal structure automatically.
Signal fields
Required
| Field | Type | Description |
|---|---|---|
mediaId | String | URL or unique identifier of the media file |
signalType | String | Signal type. Value: "richMediaInteraction". |
Optional
| Field | Type | Description |
|---|---|---|
audience | [String: Any] | Key-value pairs for contact mapping. Keys must match contact attribute names exactly as they appear in Connect, including capitalization and spacing. |
category | String | Signal category. Value: "Behavior". |
description | String | A description of the signal |
effect | String | Describes the effect of the signal on engagement. Use "positive" for rich media interactions. |
interactionType | String | The type of user interaction with the media (for example, "LAUNCH", "PAUSE", "COMPLETE"). |
mediaCategory | String | Category of the media content (for example, "product demo", "tutorial", "review") |
mediaName | String | Title of the audio or video |
name | String | A label to differentiate this signal from others (for example, "product demo interaction"). Max 256 characters |
Things to know
- If a required field is missing or invalid, the entire signal is discarded.
Basic example
import AVFoundation
import Connect
ConnectCustomEvent.sharedInstance().logSignal([
"signalType": "richMediaInteraction",
"mediaId": "product-demo-wireless-headphones",
"mediaName": "Wireless Headphones Demo",
"interactionType": "LAUNCH",
"effect": "positive"
])#import <AVFoundation/AVFoundation.h>
#import <Connect/Connect.h>
[[ConnectCustomEvent sharedInstance] logSignal:@{
@"signalType": @"richMediaInteraction",
@"mediaId": @"product-demo-wireless-headphones",
@"mediaName": @"Wireless Headphones Demo",
@"interactionType": @"LAUNCH",
@"effect": @"positive"
}];Complete example
This example instruments an AVPlayer instance to send signals on key playback events. It observes timeControlStatus via KVO to distinguish launch, pause and resume and uses NotificationCenter to detect completion. A launched flag prevents duplicate LAUNCH signals. Remove the observer and notification in deinit / dealloc to avoid memory leaks.
import AVFoundation
import Connect
final class ProductVideoViewController: UIViewController {
private var player: AVPlayer?
private var playerObservation: NSKeyValueObservation?
private var launched = false
private let mediaId: String
private let mediaName: String
init(mediaId: String, mediaName: String) {
self.mediaId = mediaId
self.mediaName = mediaName
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:)") }
override func viewDidLoad() {
super.viewDidLoad()
guard let url = URL(string: mediaId) else { return }
let item = AVPlayerItem(url: url)
let player = AVPlayer(playerItem: item)
self.player = player
// Embed player layer ...
attachSignalObservers(player: player, item: item)
}
private func attachSignalObservers(player: AVPlayer, item: AVPlayerItem) {
// Observe play/pause/buffer state via timeControlStatus KVO
playerObservation = player.observe(\.timeControlStatus, options: [.new]) { [weak self] player, _ in
guard let self else { return }
switch player.timeControlStatus {
case .playing:
let interactionType = self.launched ? "CONTINUE" : "LAUNCH"
self.launched = true
self.sendRichMediaSignal(interactionType: interactionType)
case .paused:
if self.launched {
self.sendRichMediaSignal(interactionType: "PAUSE")
}
default:
break
}
}
// Observe completion via notification
NotificationCenter.default.addObserver(
self,
selector: #selector(playerDidFinish),
name: .AVPlayerItemDidPlayToEndTime,
object: item
)
}
@objc private func playerDidFinish() {
sendRichMediaSignal(interactionType: "COMPLETE")
}
private func sendRichMediaSignal(interactionType: String) {
var signal: [String: Any] = [
"signalType": "richMediaInteraction",
"mediaId": mediaId,
"mediaName": mediaName,
"interactionType": interactionType,
"mediaCategory": "product demo",
"effect": "positive"
]
if UserSession.shared.isAuthenticated, let email = UserSession.shared.email {
signal["audience"] = ["Email Address": email]
}
let accepted = ConnectCustomEvent.sharedInstance().logSignal(signal)
NSLog("[Connect] richMediaInteraction (\(interactionType)) signal accepted: \(accepted)")
}
deinit {
playerObservation?.invalidate()
NotificationCenter.default.removeObserver(self)
}
}#import <AVFoundation/AVFoundation.h>
#import <Connect/Connect.h>
// Add these instance variables or properties to your view controller:
// AVPlayer *player;
// BOOL launched;
// NSString *mediaId;
// NSString *mediaName;
- (void)viewDidLoad {
[super viewDidLoad];
NSURL *url = [NSURL URLWithString:self.mediaId];
if (!url) return;
AVPlayerItem *item = [AVPlayerItem playerItemWithURL:url];
AVPlayer *player = [AVPlayer playerWithPlayerItem:item];
self.player = player;
// Embed player layer ...
[player addObserver:self
forKeyPath:@"timeControlStatus"
options:NSKeyValueObservingOptionNew
context:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(playerDidFinish)
name:AVPlayerItemDidPlayToEndTimeNotification
object:item];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ([keyPath isEqualToString:@"timeControlStatus"] && object == self.player) {
NSInteger status = [change[NSKeyValueChangeNewKey] integerValue];
if (status == AVPlayerTimeControlStatusPlaying) {
NSString *interactionType = self.launched ? @"CONTINUE" : @"LAUNCH";
self.launched = YES;
[self sendRichMediaSignalWithInteractionType:interactionType];
} else if (status == AVPlayerTimeControlStatusPaused) {
if (self.launched) {
[self sendRichMediaSignalWithInteractionType:@"PAUSE"];
}
}
}
}
- (void)playerDidFinish {
[self sendRichMediaSignalWithInteractionType:@"COMPLETE"];
}
- (void)sendRichMediaSignalWithInteractionType:(NSString *)interactionType {
NSMutableDictionary *signal = [@{
@"signalType": @"richMediaInteraction",
@"mediaId": self.mediaId,
@"mediaName": self.mediaName,
@"interactionType": interactionType,
@"mediaCategory": @"product demo",
@"effect": @"positive"
} mutableCopy];
if ([[UserSession shared] isAuthenticated] && [[UserSession shared] email]) {
signal[@"audience"] = @{ @"Email Address": [[UserSession shared] email] };
}
BOOL accepted = [[ConnectCustomEvent sharedInstance] logSignal:signal];
NSLog(@"[Connect] richMediaInteraction (%@) signal accepted: %@",
interactionType, accepted ? @"true" : @"false");
}
- (void)dealloc {
[self.player removeObserver:self forKeyPath:@"timeControlStatus"];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}Verification
After a media interaction occurs (play, pause, complete or seek), check the Xcode console for:
[Connect] richMediaInteraction signal accepted: true
The false value means the SDK rejected the call - see Troubleshooting below. Once the console confirms acceptance, verify the signal is available in Connect.
Troubleshooting
Signal not appearing in Connect?
- Confirm the Connect SDK is initialized before any signal calls. See Method.
- Capture the return value and check that it is
true:let accepted = ConnectCustomEvent.sharedInstance().logSignal(signal). Afalsereturn indicates the SDK rejected the call. - Verify the
appKeyandpostURLmatch the Connect org you're checking.
Signal marked as invalid in Connect?
- Confirm all required fields are present:
signalType,mediaId.
Duplicate signals for a single interaction?
AVPlayercan firetimeControlStatuschanges multiple times during a single user action. Use a guard flag (as shown withlaunched) or a debounce to prevent duplicateLAUNCHsignals. ForPAUSE, guard against firing before the first play by checkinglaunchedbefore sending.
Memory leak after dismissal?
- Invalidate the KVO observation (
playerObservation?.invalidate()) and removeNotificationCenterobservers indeinit, as shown in the complete example.
Contact not created or updated?
- Verify attribute key names in
audiencematch exactly how they appear in Connect - including capitalization and spacing.
Related pages
Updated about 1 hour ago
