# MigrationGuideV3

## 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
- Plugins
- Loading Content
- Attaching Event Listeners
- DRM
- Plugin Configuration
- Track Management
- Thumbnails
- Analytics
- Reset player
- Metadata
- Disabling Picture in Picture
- 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

    // 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

    // 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

    // 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

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

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

## Loading content

Version 3

    // 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

    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

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

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

    @end

Version 4

    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

    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

    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

    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

    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

    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

    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

    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

    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

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

Version 4

    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

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

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

Version 4

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

    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

    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

    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

    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

    [self.player reset:YES];

Version 4

    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

    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.

    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.

    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.

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

    //...

    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" {
                // ...
            }
        }
    }