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:

ValueWhen to fire
LOADMedia begins loading
LAUNCHUser initiates playback for the first time
PAUSEUser pauses playback
CONTINUEUser resumes playback after pausing
COMPLETEUser reaches the end of the media
STOPUser stops playback before completion
ENLARGEUser 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]?) -> Bool

Sends 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.

Signal fields

Required

FieldTypeDescription
mediaIdStringURL or unique identifier of the media file
signalTypeStringSignal type. Value: "richMediaInteraction".

Optional

FieldTypeDescription
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.
categoryStringSignal category. Value: "Behavior".
descriptionStringA description of the signal
effectStringDescribes the effect of the signal on engagement. Use "positive" for rich media interactions.
interactionTypeStringThe type of user interaction with the media (for example, "LAUNCH", "PAUSE", "COMPLETE").
mediaCategoryStringCategory of the media content (for example, "product demo", "tutorial", "review")
mediaNameStringTitle of the audio or video
nameStringA 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). A false return indicates the SDK rejected the call.
  • Verify the appKey and postURL match 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?

  • AVPlayer can fire timeControlStatus changes multiple times during a single user action. Use a guard flag (as shown with launched) or a debounce to prevent duplicate LAUNCH signals. For PAUSE, guard against firing before the first play by checking launched before sending.

Memory leak after dismissal?

  • Invalidate the KVO observation (playerObservation?.invalidate()) and remove NotificationCenter observers in deinit, as shown in the complete example.

Contact not created or updated?

  • Verify attribute key names in audience match exactly how they appear in Connect - including capitalization and spacing.

Related pages


Did this page help you?