## Migration guide from v3

PRESTOplay SDK for Apple version 4 has been re-designed from the ground up to build a faster and smaller player SDK. We focused on the core components of video playback and the underlying media playback engine.

The result is a small, performant core that can be easily integrated in any Application Environment. Skinning the player and building an interface can be done with your favorite UI framework without any workarounds and limitations.

Since the new architecture was only possible with drastic measures, we needed to adopt and change the previous API. The following guide is intended to enable a smooth migration from our version 3.x to version 4.x of our SDK and highlight some of the key differences.

## Table of contents

- [Player Initialization](#player-initialization)
- [Plugins](#plugins)
- [Loading Content](#loading-content)
- [Attaching Event Listeners](#attaching-event-listeners)
- [DRM](#drm)
- [Plugin Configuration](#plugin-configuration)
- [Track Management](#track-management)
- [Thumbnails](#thumbnails)
- [Analytics](#analytics)
- [Reset player](#reset-player)
- [Metadata](#metadata)
- [Disabling Picture in Picture](#disabling-picture-in-picture)
- [Error handling](#error-handling)

## Player initialization

The minimal required configuration to initialize the SDK remains your `Castlabs` license. In version 4, we utilize the singleton pattern and expose the `shared` property from PRESTOplay SDK, which differs from the factory pattern used on the previous version.

Version 3

```objc
// init sdk
[CastlabsSDK with:@[]
             andLicenseKey:@"LICENSE"
             andDelegate:nil];

// get player
CLPlayerFactory* factory = [[CLPlayerFactory alloc] init];
CLPlayer* player = [factory createPlayerWithStreamUrl:<stream_url>
                            andContentType:<content_type>];
```

Version 4

```swift
// init sdk
let licenseCheck = PRESTOplaySDK.shared.setup("LICENSE", [])

// get player
let player = PRESTOplaySDK.shared.player()
```

`licenseCheck` result `PRESTOerror` maps v3 `CastlabsSDK` init results.
`licenseCheck` will be `nil` if v4 SDK is initialized properly.

In case of error `PRESTOerror` contains compatibility `data`:

- `"v3compatibility_event"`
- `"v3compatibility_code"`

Global `SDK_ERROR_OPERATION_TIMEOUT` is migrated to each operation status individually in v4.

## Plugins

The Apple SDK provides plugins which, added to the player, enable playback of different types of content and/or add new functionality to the player. The plugins are self-contained bundles that add a specific capability to the core player and also provide integrations with third party libraries or services. Below is an example on how to use `HLSPlugin` to enable playback of HLS content.

Version 3

```objc
// init sdk
[CastlabsSDK with:@[<other_plugins>]
             andLicenseKey:@"LICENSE"
             andDelegate:nil];

// get player
CLPlayerFactory* factory = [[CLPlayerFactory alloc] init];
CLPlayer* player = [factory createPlayerWithStreamUrl:<stream_url>
                            andContentType:kContentHLS];
```

Version 4

```swift
// init sdk
_ = PRESTOplaySDK.shared.setup("LICENSE", [HLSPlugin(),])

// get player
let player = PRESTOplaySDK.shared.player()
```

## Loading content

Version 3

```objc
// init sdk
[CastlabsSDK with:@[<other_plugins>]
             andLicenseKey:@"LICENSE"
             andDelegate:nil];

CLPlayerFactory* factory = [[CLPlayerFactory alloc] init];

// get a player for the given stream url
CLPlayer* player = [factory createPlayerWithStreamUrl:<stream_url>
                            andContentType:<content_type>];

// or
CLPlayer* player = [factory createPlayerWithStreamUrl:<stream_url>
                            andDrmConfiguration:<drm_configuration>
                            andContentType:<content_type>];

// or
CLPlayer* player = [factory createPlayerWithStreamUrl:<stream_url>
                            andDrmConfiguration:<drm_configuration>
                            andContentMetadata:<content_metadata>
                            andContentType:<content_type>];
```

Version 4

```swift
let contentUrl = URL(string: "<stream_url>")!
var config = PlayerConfiguration(with: contentUrl)

// set metadata/drm configuration
config.drmType = .drmToday
config.drmSystem = .fairplay
config.drmConfiguration = DRMConfiguration() // ...
config.metaData = MetaData() // ...

// ...
let player = PRESTOplaySDK.shared.player()
player.load(config: config)
```

## Attaching event listeners

Version 3

```objc
// the view controller implements `CLPlayerListenerProtocol`
@interface PlayerViewController() <CLPlayerListenerProtocol> {}
@end

@implementation PlayerViewController
// ...
- (void) onStateChangedTo:(StateID)newState from:(StateID)oldState withData:(NSDictionary*)data {
    // ...
}

@end
```

Version 4

```swift
class PlayerViewController: UIViewController {
  var player = PRESTOplaySDK.shared.player()

  let contentUrl = URL(string: "<stream_url>")!
  var config = PlayerConfiguration(with: contentUrl)

  // ...
  player.attach(to: view.layer)

  player.onState = { [weak self] previous, state in
    guard let self else { return }
    if let error = player.error {
        print("Error \(error)")
    }
  }

  player.load(config: config)
}
```

## DRM

Version 3

```objc
CLDrmConfiguration * drmConfig = [[CLDrmConfiguration alloc] init];
drmConfig.assetId = @"...";
drmConfig.merchantId = @"...";
drmConfig.sessionId = @"...";
drmConfig.userId = @"...";
drmConfig.environment = kProduction;
drmConfig.type = kDrmFairplay;

CLPlayer* player = [factory createPlayerWithStreamUrl:@"https://example.com/main.m3u8"
                            andDrmConfiguration:drmConfig
                            andContentType:kContentHLS];
// ...
[self.player open];
```

Version 4

```swift
var player = PRESTOplaySDK.shared.player()

let contentUrl = URL(string: "https://example.com/main.m3u8")!

var drmConfig = DRMConfiguration()
drmConfiguration.environment = .production
drmConfiguration.userId = "..."
drmConfiguration.sessionId = "..."
drmConfiguration.merchant = "..."
drmConfiguration.assetId = "..."
drmConfiguration.environment = "..."

var config = PlayerConfiguration(with: contentUrl)
config.drmType = .drmToday
config.drmSystem = .fairplay
config.drmConfiguration = drmConfig // ...
config.metaData = MetaData() // ...

// ...
player.load(config: config)
```

## Plugin configuration

Version 3

```objc
NSMutableArray *plugins = [NSMutableArray arrayWithObjects:
                                          [CastlabsApple alloc],
                                          nil];
// e.g. init youbora plugin
[plugins addObject:[[CastlabsYoubora alloc] initWithKeys:@{
                    @"accountCode": @"<account_code>"
                  }]];

[CastlabsSDK with:plugins
             andLicenseKey:@"LICENSE"
             andDelegate:nil];
```

Version 4

```swift
var plugins: [CLPluginProtocol] = [HLSPlugin(),]

// init youbora plugin
let youboraSettings = YouboraSettings(accountCode: "<account_code>")
plugins.append(YouboraPlugin(youboraSettings))

// init sdk
_ = PRESTOplaySDK.shared.setup("LICENSE", plugins)

// get player
let player = PRESTOplaySDK.shared.player()
```

## Track management

Version 3

```objc
int nrAudioTracks = [player numberOfAudioTracks];
NSMutableArray *audioTracks = [NSMutableArray array];
for (int i=0; i < nrAudioTracks; i++) {
  CLPlayerAudioTrack *audioTrack = [player describeAudioTrack:i];
  [audioTracks addObject:audioTrack];
}
// ...
```

Version 4

```swift
let audioTracks: [AudioTrack] = player.tracks.audio
// ...
```

## Track model

One of the significant changes in Version 4 is around the track model. The new track model now differentiates between tracks and renditions. A track now represents a set of streams of a particular type (video, audio, text). A track contains one or many renditions. A rendition is a specific stream for a target quality level or bitrate.

### Track information

Version 3

```objc
int nrAudioTracks = [player numberOfAudioTracks];
NSMutableArray *audioTracks = [NSMutableArray array];
for (int i=0; i < nrAudioTracks; i++) {
    CLPlayerAudioTrack *audioTrack = [player describeAudioTrack:i];
    [audioTracks addObject:audioTrack];
    NSLog(@"AudioTracks, codec: %@ index: %ld", audioTrack.codec, (long)audioTrack.trackIndex);
}
// ...
```

Version 4

```swift
var tracks: [Track] = player.tracks.all
var videoTracks: [VideoTrack] = player.tracks.video

var selectedVideoTrack = player.getVideoTrack()
var selectedVideoRendition = player.getSelectedVideoRendition()

print("Selected video rendition, codec: \(selectedVideoRendition.codec)")

for track in tracks {
  // print track properties ...
  print("Track info: \(track.label) ...")
}
```

### Track selection

Version 3

```objc
// get track index from player API
// e.g. int audioIndex = 1 ...
// ...
player.selectAudio(audioIndex);
player.selectVideo(videoIndex);
player.selectText(textIndex);
```

Version 4

```swift
let videoTrack = player.tracks.video.first!
let audioTrack = player.tracks.audio.first!
let textTrack = player.tracks.text.first!

// ...
player.setVideoTrack(videoTrack)
player.setAudioTrack(audioTrack)
player.setTextTrack(textTrack)
```

### Sideloaded text tracks

Version 3

```objc
[player addSubtitlesTrackFromUrl:[NSURL URLWithString:@"<subtitles_url>"]
                  withFormat:kSubtitleWebVTT
                  withDisplayName:@"Sideloaded subtitles"
                  withLanguageCode:@"en"];

// use player APIs to select the sideloaded track ...
```

Version 4

```swift
// ...
let subtitles = PRESTOplaySDK.shared.subtitles(for: player)

let textTrack = TextTrack(
                  id: UUID().uuidString,
                  format: .web_vtt,
                  isPlatformRendered: false,
                  label: "Sideloaded subtitles",
                  language: "en",
                  sourceUrl: URL(string: "<subtitles_url>")!)

subtitles?.addTextTrack(textTrack, completionHandler: { error in
    if let error {
        print(error)
    } else {
        print("subtitles track loaded")
    }
    DispatchQueue.main.async {
        self.player.setTextTrack(subtitlesTrack)
    }
})
```

## Thumbnails

Version 3

```objc
CLPlayer* player = // ...
[player setThumbnailWithWebVTTtrack:@"https://example.com/thumbs.vtt"];

// Get a thumbnail at position 12 (seconds)
CLThumb* thumbnail = [player getThumb: CMTimeMakeWithSeconds(12, 1)];
// ...
```

Version 4

```swift
let player = PRESTOplaySDK.shared.player()
let thumbnails = PRESTOplaySDK.shared.thumbnails(for: player)
let thumbnailsUrl = URL(string: "https://example.com/thumbs.vtt")!

thumbnails?.loadThumbnailsFrom(webVTTtrack: thumbnailsUrl, completionHandler: { error in
    if let error { print(error) } else { print("thumbnails loaded") }
})

// ...
let timestamp = CMTime(seconds: 12, preferredTimescale: 1)
let thumbnail = thumbnails?.getThumbnail(atTimestamp: timestamp)
let image = UIImage(data: thumbnail.imageData)
```

## Analytics

An example of using Youbora analytics plugin and how to pass content metadata.

Version 3

```objc
CLContentMetadata* metadata = [[CLContentMetadata alloc] initWithContentAssetId:@"asset_1" title:@"Test Stream" isLive:NO];
CLPlayerFactory* factory = [[CLPlayerFactory alloc] init];

// pass the content metadata to the factory method
CLPlayer* player = [factory createPlayerWithStreamUrl:@"<stream_url>"
                            andDrmConfiguration:<drm_config>
                            andContentMetadata:metadata
                            andContentType:kContentHLS];
```

Version 4

```swift
let player = PRESTOplaySDK.shared.player()

// ...
var analytics = PRESTOplaySDK.shared.analytics(for: player, .youbora)

let youboraMetadata = YouboraMetadata(
    live: false,
    assetId: "assetId")
youboraMetadata.analyticsOptions = NpawPlugin.AnalyticsOptions()
youboraMetadata.analyticsOptions?.adCustomDimension1 = "test"
youboraMetadata.analyticsOptions?.contentTitle = "test"
youboraMetadata.analyticsOptions?.autoDetectBackground = false
youboraMetadata.accountCode = "ACCOUNT_CODE"

analytics?.metadata = youboraMetadata
```

## Reset player

To achieve the same behavior as `reset()` in V3, use the `load(config:)` method. To change the starting position, set the `startTimeMs` value.

Version 3

```objc
[self.player reset:YES];
```

Version 4

```swift
let player = PRESTOplaySDK.shared.player()
player.load(config: playerConfig)

// ...
playerConfig.startTimeMs = 10000
player.load(config: playerConfig)
```

## Metadata

In V4, `CLContentMetadata` from V3 is converted to `AnalyticsMetadata`. And it is set on the plugin instance instead of player.

Version 3

```objc
player.metadata = [[CLContentMetadata alloc] initWithContentAssetId:config.identifier title:config.metadata.title isLive:config.live];
player.metadata.imageUrl = config.metadata.thumbUrl;
player.metadata.imageWidht = 173;
player.metadata.imageHeight = 256;
player.metadata.viewerId = "viewer id";
```

Version 4

Configure the metadata for Conviva plugin. It is similar for other plugins.

```swift
var analytics = PRESTOplaySDK.shared.analytics(for: player, .conviva)
let metadata = ConvivaMetadata( live: false, assetId: "assetId")
metadata.assetName = "asset name"
analytics.metadata = metadata
```

Configure the metadata for Chromecast plugin.

```swift
import CastlabsChromecast
import GoogleCast

let castSettings = CastSettings(appId)
var cast = PRESTOplaySDK.shared.cast(for: castSettings)

let metadata = ChromecastMetadata(live: false, assetId: "assetId")
metadata.title = "title"
metadata.subtitle = "subtitle"
metadata.mediaMetadata = GCKMediaMetadata()
// Set mediaMetadata properties.
// ...
metadata.mediaMetadata.setString(title, forKey: "title")
// customData passed directly to the player
// ...
metadata.extra = [
    "startTime": Int(startTime),
    "endTime": Int(endTime),
    "isLive": isLive,
    "isLiveTimeShifted": isOnAir,
    "widevine": widevineDeviceId,
    "licenseUrl": licenseUrl,
    "receiverName": CastlabsUtils.receiverName
]
cast?.metadata = metadata
```

## Disabling picture in picture

In Version 3, Picture in Picture could be disabled using the `disablePictureInPicture` flag. In Version 4, Picture in Picture must be initialized by calling `PlayerAPI.getPictureInPictureController()` method. If this method is not called, Picture in Picture will be disabled by default.

```swift
let pipController = player.getPictureInPictureController()
pipController.delegate = self
```

## Error handling

In version 3, error handling is distributed across different callbacks or listeners, with each plugin reporting errors through its own mechanism.

Starting with version 4, all errors are centralized and delivered through a single `onError` callback. This means:

- All SDK-related errors, regardless of the source plugin, are reported through `PRESTOplaySDK.shared.onError`.
- Each error includes metadata that identifies the originating plugin and provides additional context.

This unified approach simplifies error handling and ensures a consistent structure across all components of the SDK.

E.g.:

```swift
//...

PRESTOplaySDK.shared.onError = { [weak self] _, error in
    guard let self else { return }
    if error.type == .ads_error {
        // handle ad errors
        if error.data.keys.contains("errorType"), error.data["errorType"] == "adLoadingFailed" {
            // ...
        }
    }
}
```
