Skip to main content

Cloud Bookmarks

Bookmarks is a personalization library that provides cloud bookmarking services to enable Continue Watching / Continue Listening experiences.

The library helps persist last watched position for VOD content and provides APIs to retrieve, update, and delete bookmark positions.

[!IMPORTANT] Bookmarks are not applicable for Live playback. They are also not intended for promo/ad playback.

Automatic enforcement with BookmarksManager

BookmarksManager can automate bookmark updates using player state/progress callbacks.

Create BookmarkSessionConfiguration

BookmarkSessionConfiguration defines sync and deletion behavior.

PropertyTypeDefaultDescription
endPointStringN/ABookmarks microservice endpoint URL.
bookmarkSyncIntervalTimeInterval60000Sync interval in milliseconds.
bookmarkThresholdPercentageDouble?0.95Fully-watched threshold as percentage factor.
thresholdPlaybackPositionMsTimeInterval?nilAbsolute position threshold in milliseconds.
let config = FLBookmarksFactory.bookmarkSessionConfiguration(
endPoint: bookmarkEndPointURL,
bookmarkSyncInterval: 15_000,
bookmarkThresholdPercentage: 0.98,
thresholdPlaybackPositionMs: 1_800_000
)

Create BookmarkAttributes

Use BookmarkAttributes to provide content metadata for manager-driven updates. The structure depends on the content type.

Normal content (movie / standalone VOD)

For standalone VOD content, provide only itemId, offset, and optionally contentType. Leave other optional fields as nil.

Per Personalization API, request body expects: itemId, offset

let normalAttributes = BookmarkAttributes(
itemId: "movie-123",
seasonId: nil,
episodeId: nil,
videoId: nil,
offset: 120_000, // milliseconds
mode: nil,
seasonNumber: nil,
episodeNumber: nil,
contentType: "movie"
)

Series / episodic content

For episodic content, provide itemId, offset, seasonId, episodeId, mode: .series, and optionally season/episode numbers and content type.

Per Personalization API, request body expects: itemId, offset, seasonId, episodeId, mode: "series" (or "episode")

let seriesAttributes = BookmarkAttributes(
itemId: "series-content-id",
seasonId: "season-1-id",
episodeId: "episode-3-id",
videoId: nil,
offset: 120_000, // milliseconds
mode: .series,
seasonNumber: 1,
episodeNumber: 4,
contentType: "tvepisode"
)

Playlist content

For playlist content, provide itemId (playlist ID), videoId (video in the playlist), offset, and mode: .playlist.

Per Personalization API, request body expects: itemId, offset, videoId, mode: "playlist"

let playlistAttributes = BookmarkAttributes(
itemId: "playlist-id",
seasonId: nil,
episodeId: nil,
videoId: "video-in-playlist-id",
offset: 120_000, // milliseconds
mode: .playlist,
seasonNumber: nil,
episodeNumber: nil,
contentType: nil
)

Create BookmarksManager

  1. Always Prefer the bookmarkAttributes overload (the itemId overload is deprecated).
let bookmarksManager = FLBookmarksFactory.bookmarksManager(
configuration: config,
bookmarkAttributes: attributes,
authorizer: platformAuthorizer,
httpClient: FLFoundationFactory.httpClient() // optional
)
  1. If your app already created a BookmarkService, you can construct manager with the bookmarkService overload as well. If you use this to create bookmarkManager, refer final section of this doc to find manual bookmark manager creation. Do not prefer this, always prefer the above bookmarksManager creation unless its absolutely necessary.
let bookmarkService = FLBookmarksFactory.bookmarkService(
endPoint: bookmarkEndPointURL,
authorizer: platformAuthorizer
)

let bookmarksManager = FLBookmarksFactory.bookmarksManager(
configuration: config,
bookmarkAttributes: attributes,
bookmarkService: bookmarkService
)

Hook manager callbacks into your player/composable player flow:

composablePlayer.addStateChangeBlock(bookmarksManager.processPlaybackStateChange)
composablePlayer.addHeartBeatBlock(bookmarksManager.processPlaybackProgress)

BookmarkService

BookmarkService declares APIs to record, remove, and retrieve bookmark positions. Because bookmark calls are authorized network calls, provide a PlatformAuthorizer when creating the service.

import FLBookmarks
import FLFoundation
import FLPlatformCore

let bookmarkEndPointURL = "https://example.com/" // include trailing slash

let bookmarkService: BookmarkService = FLBookmarksFactory.bookmarkService(
endPoint: bookmarkEndPointURL,
httpClient: FLFoundationFactory.httpClient(), // optional
authorizer: platformAuthorizer
)

Retrieve last watched position

Applications usually fetch bookmark position before playback starts (for example, content details page, continue watching rails).

Response model

All retrieve APIs return one or more BookmarkRecord values.

FieldTypeDescription
itemIdStringUnique identifier of the content item.
offsetTimeIntervalLast watched position in milliseconds.
updatedTimestampTimeInterval?Server timestamp when bookmark was last updated.
seasonIdString?Season identifier (episodic content).
episodeIdString?Episode identifier (episodic content).
videoIdString?Video identifier (playlist use cases).
modeMode?Bookmark mode (.series, .episode, .playlist).
seasonNumberInt?Season number (episodic content).
episodeNumberInt?Episode number in season (episodic content).
contentTypeString?Content type metadata, when available.
playlistBookmarks[PlaylistBookmarkRecord]?Per-video bookmarks for playlist content.

PlaylistBookmarkRecord represents bookmark data for an individual video inside a playlist.

FieldTypeDescription
videoIdStringUnique identifier of the video in the playlist.
offsetTimeIntervalLast watched position for this video in milliseconds.
playlistIdStringIdentifier of the parent playlist.
updatedTimestampTimeIntervalServer timestamp when this video bookmark was updated.

Singular content

Use getBookmark(itemId:completion:) to fetch bookmark for one content item.

let contentId = "1234-5678-90ABC" 

bookmarkService.getBookmark(itemId: contentId) { result in
switch result {
case .success(let record):
let itemId = record.itemId
let offset = record.offset
let contentType = record.contentType
let seasonId = record.seasonId
let episodeId = record.episodeId
let seasonNumber = record.seasonNumber
let episodeNumber = record.episodeNumber
let videoId = record.videoId
let mode = record.mode
let timestamp = record.updatedTimestamp
let playlistBookmarks = record.playlistBookmarks
print(itemId, offset, contentType ?? "")

case .failure(let error):
print("getBookmark failed: \(error)")
}
}

Episodic content (series)

Use getSeriesBookmarks(seriesIdentifier:pageNumber:pageSize:sortBy:sortOrder:completion:).

let seriesId = "098-765-4321"

bookmarkService.getSeriesBookmarks(
seriesIdentifier: seriesId,
pageNumber: 1,
pageSize: 10,
sortBy: .timestamp,
sortOrder: .descending
) { result in
switch result {
case .success(let records):
records.forEach { record in
print(record.itemId, record.offset, record.episodeId ?? "")
}

case .failure(let error):
print("getSeriesBookmarks failed: \(error)")
}
}

Playlist content

Use getPlaylistBookmarks(playlistIdentifier:pageNumber:pageSize:sortBy:sortOrder:completion:).

let playlistId = "098-765-4321"

bookmarkService.getPlaylistBookmarks(
playlistIdentifier: playlistId,
pageNumber: 1,
pageSize: 10,
sortBy: .timestamp,
sortOrder: .descending
) { result in
switch result {
case .success(let records):
records.forEach { record in
print(record.itemId, record.offset, record.videoId, record.updatedTimestamp)
}

case .failure(let error):
print("getPlaylistBookmarks failed: \(error)")
}
}

Current user bookmarks

Use getBookmarks(pageNumber:pageSize:sortBy:sortOrder:completion:) to fetch incompletely watched bookmarks for the current user.

bookmarkService.getBookmarks(
pageNumber: 1,
pageSize: 10,
sortBy: .timestamp,
sortOrder: .descending
) { result in
switch result {
case .success(let records):
records.forEach { record in
print(record.itemId, record.offset)
}

case .failure(let error):
print("getBookmarks failed: \(error)")
}
}

Queue-based bookmarking

For queue playback, use the queue-capable bookmarksManager overload with a MediaPlaylistItemTransitionCallback implementation that returns AssetBookmarkConfiguration for each transition.

Usage

1. Implement MediaPlaylistItemTransitionCallback

Your client code implements MediaPlaylistItemTransitionCallback to provide bookmark configuration for each item:

class MyPlayerViewController: MediaPlaylistItemTransitionCallback {
func onMediaPlaylistItemTransition(for item: MediaPlaylistItem) -> AssetBookmarkConfiguration? {
// Return configuration for the item, or nil to skip bookmarking
}
}

2. Create QueueBookmarksManager

let configuration = FLBookmarksFactory.bookmarkSessionConfiguration(
endPoint: endPoint,
bookmarkSyncInterval: 5000,
bookmarkThresholdPercentage: 0.95
)

// Initial bookmark attributes (will be updated on transitions)
let initialAttributes = BookmarkAttributes(
itemId: "initial-id",
seasonId: nil,
episodeId: nil,
videoId: nil,
mode: nil,
offset: 120_000, // Eg: 2 minutes in milliseconds
seasonNumber: nil,
episodeNumber: nil,
contentType: ""
)

// Create queue bookmark manager with configuration provider
let queueBookmarksManager = FLBookmarksFactory.bookmarksManager(
configuration: configuration,
bookmarkAttributes: initialAttributes,
bookmarkService: bookmarkService,
configurationCallback: self // implements MediaPlaylistItemTransitionCallback
)

3. Integrate with Player

// Add state change and heartbeat blocks
player.addStateChangeBlock(block: queueBookmarksManager.processPlaybackStateChange)
player.addHeartBeatBlock(block: queueBookmarksManager.processPlaybackProgress)

4. Restoring Playback from Bookmark

Each item's bookmark is stored separately using existing bookmark APIs:

// Get bookmark for a specific item
bookmarkService.getBookmark(itemId: lastPlayedItemId) { result in
switch result {
case .success(let record):
// Restore playback from saved position
let startOffset = record.offset / 1000 // Convert ms to seconds
player.seek(to: startOffset)

case .failure:
// No bookmark exists, start from beginning
break
}
}
Manual enforcement with BookmarkService

Maintaining bookmark positions can also be manually enforced by calling BookmarkService APIs directly when the application needs explicit control over when bookmarks are recorded or removed.

Applications can directly call service APIs to upsert or remove bookmarks. Prefer BookmarksManager for most integrations, since it automatically handles periodic bookmark put/delete operations.

Payload mapping

Content typePersonalization API expectationiOS putBookmark fields
Normal content (movie / standalone VOD)itemId, offsetitemId, offset, and keep seasonId, episodeId, videoId, mode as nil
Series / episodic contentitemId, offset, seasonId, episodeId, mode = "series" (or episode mode where applicable)itemId, offset, seasonId, episodeId, mode: .series (or .episode when your flow is episode-specific)
Playlist contentitemId, offset, videoId, mode = "playlist"itemId, offset, videoId, mode: .playlist

Normal content vs series bookmark vs playlist bookmark

  • Normal content bookmark is for a single asset. Retrieve with getBookmark(itemId:) and delete with deleteBookmark(itemId:completion:).
  • Series bookmark carries series metadata (seasonId, episodeId, mode) and can be listed via getSeriesBookmarks(seriesIdentifier:...) and deleted with deleteBookmark(itemId:episodeId:mode:completion:)(using mode: .episode for episode-specific deletion).
  • Playlist bookmark carries playlist metadata (videoId, mode) and can be listed via getPlaylistBookmarks(playlistIdentifier:...) and deleted with deleteBookmark(itemId:videoId:mode:completion:)(using mode: .playlist for playlist-video deletion).
let contentId = "123ABC-LC131-PLOJ45"
let latestPositionMs: TimeInterval = 99_999

bookmarkService.putBookmark(
itemId: contentId,
offset: latestPositionMs,
seasonId: nil,
episodeId: nil,
videoId: nil,
mode: nil,
seasonNumber: nil,
episodeNumber: nil,
contentType: nil
) { result in
switch result {
case .success:
print("Bookmark recorded for \(contentId) at \(latestPositionMs)ms")
case .failure(let error):
print("putBookmark failed: \(error)")
}
}

bookmarkService.deleteBookmark(itemId: contentId) { result in
switch result {
case .success:
print("Bookmark deleted for \(contentId)")
case .failure(let error):
print("deleteBookmark failed: \(error)")
}
}