From 0029ac40bd855795b29e2af659c60c0dbda778e1 Mon Sep 17 00:00:00 2001 From: Stefano Russello Date: Mon, 2 Dec 2024 13:24:39 +0100 Subject: [PATCH] CORE-4594 Playlist API integration - New public PlaybackVideoDetails class as @artem-y-pamediagroup suggested on PR #26 - Rollback PlaybackResponseModel to internal struct - Renaming PlayBack API folder to Playback API - Added userAgent param comment on PlaybackAPI and PlaybackAPIService - Fixed indentation on parameters comments --- .../PlaybackAPI.swift | 5 +- .../PlaybackAPIService.swift | 1 + .../PlaybackResponseModel.swift | 11 ++++ .../Playback API/PlaybackVideoDetails.swift | 28 +++++++++ Sources/PlaybackSDK/PlaybackSDKManager.swift | 61 +++++++++++-------- .../BitMovinPlugin/BitmovinPlayerPlugin.swift | 12 ++-- .../BitMovinPlugin/BitmovinPlayerView.swift | 52 +++++++++------- .../Player Plugin/VideoPlayerPlugin.swift | 5 +- .../PlayerUIView/PlaybackUIView.swift | 21 ++++--- 9 files changed, 131 insertions(+), 65 deletions(-) rename Sources/PlaybackSDK/{PlayBack API => Playback API}/PlaybackAPI.swift (66%) rename Sources/PlaybackSDK/{PlayBack API => Playback API}/PlaybackAPIService.swift (95%) rename Sources/PlaybackSDK/{PlayBack API => Playback API}/PlaybackResponseModel.swift (82%) create mode 100644 Sources/PlaybackSDK/Playback API/PlaybackVideoDetails.swift diff --git a/Sources/PlaybackSDK/PlayBack API/PlaybackAPI.swift b/Sources/PlaybackSDK/Playback API/PlaybackAPI.swift similarity index 66% rename from Sources/PlaybackSDK/PlayBack API/PlaybackAPI.swift rename to Sources/PlaybackSDK/Playback API/PlaybackAPI.swift index 187b1ef..f76ffe6 100644 --- a/Sources/PlaybackSDK/PlayBack API/PlaybackAPI.swift +++ b/Sources/PlaybackSDK/Playback API/PlaybackAPI.swift @@ -17,8 +17,9 @@ internal protocol PlaybackAPI { Retrieves video details for a given entry ID. - Parameters: - - entryId: The unique identifier of the video entry. - - andAuthorizationToken: Optional authorization token, can be nil for free videos. + - entryId: The unique identifier of the video entry. + - andAuthorizationToken: Optional authorization token, can be nil for free videos. + - userAgent: Custom `User-Agent` header to use with playback requests. Can be used if there was a custom header set to start session request. Defaults to `nil` - Returns: A publisher emitting a result with a response model with an error or a critical error. */ func getVideoDetails(forEntryId entryId: String, andAuthorizationToken: String?, userAgent: String?) -> AnyPublisher, Error> diff --git a/Sources/PlaybackSDK/PlayBack API/PlaybackAPIService.swift b/Sources/PlaybackSDK/Playback API/PlaybackAPIService.swift similarity index 95% rename from Sources/PlaybackSDK/PlayBack API/PlaybackAPIService.swift rename to Sources/PlaybackSDK/Playback API/PlaybackAPIService.swift index 083d9d6..65ac20e 100644 --- a/Sources/PlaybackSDK/PlayBack API/PlaybackAPIService.swift +++ b/Sources/PlaybackSDK/Playback API/PlaybackAPIService.swift @@ -31,6 +31,7 @@ internal class PlaybackAPIService: PlaybackAPI { - Parameters: - entryId: The unique identifier of the video entry. - andAuthorizationToken: Optional authorization token, can be nil for free videos. + - userAgent: Custom `User-Agent` header to use with playback requests. Can be used if there was a custom header set to start session request. Defaults to `nil` - Returns: A publisher emitting a result with a response model with an error or a critical error. */ func getVideoDetails( diff --git a/Sources/PlaybackSDK/PlayBack API/PlaybackResponseModel.swift b/Sources/PlaybackSDK/Playback API/PlaybackResponseModel.swift similarity index 82% rename from Sources/PlaybackSDK/PlayBack API/PlaybackResponseModel.swift rename to Sources/PlaybackSDK/Playback API/PlaybackResponseModel.swift index d7743ec..874904c 100644 --- a/Sources/PlaybackSDK/PlayBack API/PlaybackResponseModel.swift +++ b/Sources/PlaybackSDK/Playback API/PlaybackResponseModel.swift @@ -64,4 +64,15 @@ internal struct PlaybackResponseModel: Decodable { } } + +extension PlaybackResponseModel { + func toVideoDetails() -> PlaybackVideoDetails? { + if let entryId = self.entryId { + let videoDetails = PlaybackVideoDetails(videoId: entryId, url: self.media?.hls, title: self.name, thumbnail: self.coverImg?._360?.absoluteString, description: self.description) + return videoDetails + } + return nil + } +} + #endif diff --git a/Sources/PlaybackSDK/Playback API/PlaybackVideoDetails.swift b/Sources/PlaybackSDK/Playback API/PlaybackVideoDetails.swift new file mode 100644 index 0000000..dfdf764 --- /dev/null +++ b/Sources/PlaybackSDK/Playback API/PlaybackVideoDetails.swift @@ -0,0 +1,28 @@ +// +// PlaybackVideoDetails.swift +// PlaybackSDK +// +// Created by Stefano Russello on 29/11/24. +// + +#if !os(macOS) +import Foundation + +public class PlaybackVideoDetails { + + public var videoId: String + public var url: String? + public var title: String? + public var thumbnail: String? + public var description: String? + + public init(videoId: String, url: String? = nil, title: String? = nil, thumbnail: String? = nil, description: String? = nil) { + self.videoId = videoId + self.url = url + self.title = title + self.thumbnail = thumbnail + self.description = description + } +} + +#endif diff --git a/Sources/PlaybackSDK/PlaybackSDKManager.swift b/Sources/PlaybackSDK/PlaybackSDKManager.swift index 81f4da5..e22563e 100644 --- a/Sources/PlaybackSDK/PlaybackSDKManager.swift +++ b/Sources/PlaybackSDK/PlaybackSDKManager.swift @@ -129,15 +129,16 @@ public class PlaybackSDKManager { /// Initializes the `PlaybackSDKManager`. public init() {} - /// Initializes the SDK with the provided API key. - /// This fuction must be called in the AppDelegate - /// - /// - Parameters: - /// - apiKey: The API key for initializing the SDK. - /// - baseURL: The base URL for API endpoints. Defaults to `nil`. - /// - userAgent: Custom `User-Agent` header to use with playback requests. Can be used if there was a custom header set to start session request. Defaults to `nil` - /// - completion: A closure to be called after initialization. - /// It receives a result indicating success or failure. + /** + Initializes the SDK with the provided API key. + This fuction must be called in the AppDelegate + + - Parameters: + - apiKey: The API key for initializing the SDK. + - baseURL: The base URL for API endpoints. Defaults to `nil`. + - userAgent: Custom `User-Agent` header to use with playback requests. Can be used if there was a custom header set to start session request. Defaults to `nil` + - completion: A closure to be called after initialization. It receives a result indicating success or failure. + */ public func initialize(apiKey: String, baseURL: String? = nil, userAgent: String? = nil, completion: @escaping (Result) -> Void) { guard !apiKey.isEmpty else { completion(.failure(SDKError.initializationError)) @@ -161,15 +162,16 @@ public class PlaybackSDKManager { Loads a video player with the specified entry ID and authorization token. - Parameters: - - entryID: The unique identifier of the video entry to be loaded. - - authorizationToken: The token used for authorization to access the video content. - - onError: Return potential playback errors that may occur during the loading process. + - entryID: The unique identifier of the video entry to be loaded. + - authorizationToken: The token used for authorization to access the video content. + - onError: Return potential playback errors that may occur during the loading process. - Returns: A view representing the video player configured with the provided entry ID and authorization token. Example usage: ```swift let playerView = loadPlayer(entryID: "exampleEntryID", authorizationToken: "exampleToken") + ``` */ public func loadPlayer( entryID: String, @@ -189,16 +191,17 @@ public class PlaybackSDKManager { Loads a video player with the specified entry ID and authorization token. - Parameters: - - entryIDs: A list of the videos to be loaded. - - entryIDToPlay: The first video Id to be played. If not provided, the first video in the entryIDs array will be played. - - authorizationToken: The token used for authorization to access the video content. - - onErrors: Return a list of potential playback errors that may occur during the loading process for single entryId. + - entryIDs: A list of the videos to be loaded. + - entryIDToPlay: The first video Id to be played. If not provided, the first video in the entryIDs array will be played. + - authorizationToken: The token used for authorization to access the video content. + - onErrors: Return a list of potential playback errors that may occur during the loading process for single entryId. - Returns: A view representing the video player configured with the provided entry ID and authorization token. Example usage: ```swift let playerView = loadPlayer(entryIDs: ["exampleEntryID1", "exampleEntryID2"], authorizationToken: "exampleToken") + ``` */ public func loadPlaylist( entryIDs: [String], @@ -367,40 +370,44 @@ public class PlaybackSDKManager { } } - func createSource(from details: PlaybackResponseModel, authorizationToken: String?) -> Source? { - - guard let hlsURLString = details.media?.hls, let hlsURL = URL(string: hlsURLString) else { + func createSource(from details: PlaybackVideoDetails, authorizationToken: String?) -> Source? { + + guard let hlsURLString = details.url, let hlsURL = URL(string: hlsURLString) else { return nil } let sourceConfig = SourceConfig(url: hlsURL, type: .hls) + // Avoiding to fill all the details (title, thumbnail and description) for now // Because when the initial video is not the first one and we have to seek the first source // Bitmovin SDK has a bug/glitch that show the title/thumbnail of the first video for a short time before changing to the new one -// sourceConfig.title = details.name -// sourceConfig.posterSource = details.coverImg?._360 +// sourceConfig.title = details.title +// if let thumb = details.thumbnail, let thumbUrl = URL(string: thumb) { +// sourceConfig.posterSource = thumbUrl +// } // sourceConfig.sourceDescription = details.description - if details.entryId?.isEmpty == false { - sourceConfig.metadata["entryId"] = details.entryId + if details.videoId.isEmpty == false { + sourceConfig.metadata["entryId"] = details.videoId } else { - // Recover entryId from hls url (not working for live url) + // If entryId is null, get the entryId from HLS url (not working for live url) let regex = try! NSRegularExpression(pattern: "/entryId/(.+?)/") let range = NSRange(location: 0, length: hlsURLString.count) if let match = regex.firstMatch(in: hlsURLString, options: [], range: range) { if let swiftRange = Range(match.range(at: 1), in: hlsURLString) { - let entryId = hlsURLString[swiftRange] - sourceConfig.metadata["entryId"] = String(entryId) - + let entryIdFromUrl = hlsURLString[swiftRange] + sourceConfig.metadata["entryId"] = String(entryIdFromUrl) } } } + // Adding extra details sourceConfig.metadata["details"] = details sourceConfig.metadata["authorizationToken"] = authorizationToken return SourceFactory.createSource(from: sourceConfig) } + } diff --git a/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerPlugin.swift b/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerPlugin.swift index 91377ab..a35f16c 100644 --- a/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerPlugin.swift +++ b/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerPlugin.swift @@ -54,7 +54,7 @@ public class BitmovinPlayerPlugin: VideoPlayerPlugin, ObservableObject { playerConfig.styleConfig.userInterfaceConfig = uiConfig } - public func playerView(videoDetails: [PlaybackResponseModel], entryIDToPlay: String?, authorizationToken: String?) -> AnyView { + public func playerView(videoDetails: [PlaybackVideoDetails], entryIDToPlay: String?, authorizationToken: String?) -> AnyView { self.authorizationToken = authorizationToken self.entryIDToPlay = entryIDToPlay // Create player based on player and analytics configurations @@ -211,9 +211,13 @@ public class BitmovinPlayerPlugin: VideoPlayerPlugin, ObservableObject { if let entryId = entryId { PlaybackSDKManager.shared.loadHLSStream(forEntryId: entryId, andAuthorizationToken: authorizationToken) { result in switch result { - case .success(let videoDetails): - let newSource = PlaybackSDKManager.shared.createSource(from: videoDetails, authorizationToken: authorizationToken) - completion(newSource) + case .success(let response): + if let videoDetails = response.toVideoDetails() { + let newSource = PlaybackSDKManager.shared.createSource(from: videoDetails, authorizationToken: authorizationToken) + completion(newSource) + } else { + completion(nil) + } case .failure: break completion(nil) diff --git a/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerView.swift b/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerView.swift index 26ed9fd..e4ea3eb 100644 --- a/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerView.swift +++ b/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerView.swift @@ -38,14 +38,19 @@ public struct BitmovinPlayerView: View { return sConfig } - /// Initializes the view with the player passed from outside. - /// - /// This version of the initializer does not modify the `player`'s configuration, so any additional configuration steps - /// like setting `userInterfaceConfig` should be performed externally. - /// - /// - parameter videoDetails: Full videos details containing name, description, thumbnail, duration as well as URL of the HLS video stream that will be loaded by the player as the video source - /// - parameter player: Instance of the player that was created and configured outside of this view. - public init(videoDetails: [PlaybackResponseModel], entryIDToPlay: String?, authorizationToken: String?, player: Player) { + /** + Initializes the view with the player passed from outside. + + This version of the initializer does not modify the `player`'s configuration, so any additional configuration steps + like setting `userInterfaceConfig` should be performed externally. + + - Parameters: + - videoDetails: Full videos details containing title, description, thumbnail, duration as well as URL of the HLS video stream that will be loaded by the player as the video source + - entryIDToPlay: (Optional) The first video Id to be played. If not provided, the first video in the entryIDs array will be played. + - authorizationToken: (Optional) The token used for authorization to access the video content. + - player: Instance of the player that was created and configured outside of this view. + */ + public init(videoDetails: [PlaybackVideoDetails], entryIDToPlay: String?, authorizationToken: String?, player: Player) { self.player = player self.authorizationToken = authorizationToken @@ -55,19 +60,24 @@ public struct BitmovinPlayerView: View { sources = createPlaylist(from: videoDetails) - setupRemoteCommandCenter(title: videoDetails.first?.name ?? "") + setupRemoteCommandCenter(title: videoDetails.first?.title ?? "") } - /// Initializes the view with an instance of player created inside of it, upon initialization. - /// - /// In this version of the initializer, a `userInterfaceConfig` is being added to the `playerConfig`'s style configuration. - /// - /// - Note: If the player config had `userInterfaceConfig` already modified before passing into this `init`, - /// those changes will take no effect sicne they will get overwritten here. - /// - /// - parameter hlsURLString: Full videos details containing name, description, thumbnail, duration as well as URL of the HLS video stream that will be loaded by the player as the video source - /// - parameter playerConfig: Configuration that will be passed into the player upon creation, with an additional update in this initializer. - public init(videoDetails: [PlaybackResponseModel], entryIDToPlay: String?, authorizationToken: String?, playerConfig: PlayerConfig) { + /** + Initializes the view with an instance of player created inside of it, upon initialization. + + In this version of the initializer, a `userInterfaceConfig` is being added to the `playerConfig`'s style configuration. + + - Note: If the player config had `userInterfaceConfig` already modified before passing into this `init`, + those changes will take no effect since they will get overwritten here. + + - Parameters: + - videoDetails: Full videos details containing title, description, thumbnail, duration as well as URL of the HLS video stream that will be loaded by the player as the video source + - entryIDToPlay: (Optional) The first video Id to be played. If not provided, the first video in the entryIDs array will be played. + - authorizationToken: (Optional) The token used for authorization to access the video content. + - playerConfig: Configuration that will be passed into the player upon creation, with an additional update in this initializer. + */ + public init(videoDetails: [PlaybackVideoDetails], entryIDToPlay: String?, authorizationToken: String?, playerConfig: PlayerConfig) { let uiConfig = BitmovinUserInterfaceConfig() uiConfig.hideFirstFrame = true @@ -82,7 +92,7 @@ public struct BitmovinPlayerView: View { sources = createPlaylist(from: videoDetails) - setupRemoteCommandCenter(title: videoDetails.first?.name ?? "") + setupRemoteCommandCenter(title: videoDetails.first?.title ?? "") } public var body: some View { @@ -116,7 +126,7 @@ public struct BitmovinPlayerView: View { } } - func createPlaylist(from videoDetails: [PlaybackResponseModel]) -> [Source] { + func createPlaylist(from videoDetails: [PlaybackVideoDetails]) -> [Source] { var sources: [Source] = [] for details in videoDetails { diff --git a/Sources/PlaybackSDK/Player Plugin/VideoPlayerPlugin.swift b/Sources/PlaybackSDK/Player Plugin/VideoPlayerPlugin.swift index c0de974..b81da2e 100644 --- a/Sources/PlaybackSDK/Player Plugin/VideoPlayerPlugin.swift +++ b/Sources/PlaybackSDK/Player Plugin/VideoPlayerPlugin.swift @@ -18,10 +18,7 @@ public protocol VideoPlayerPlugin: AnyObject { func setup(config: VideoPlayerConfig) - // TODO: add event - /// func handleEvent(event: BitmovinPlayerCore.PlayerEvent) - - func playerView(videoDetails: [PlaybackResponseModel], entryIDToPlay: String?, authorizationToken: String?) -> AnyView + func playerView(videoDetails: [PlaybackVideoDetails], entryIDToPlay: String?, authorizationToken: String?) -> AnyView func play() diff --git a/Sources/PlaybackSDK/PlayerUIView/PlaybackUIView.swift b/Sources/PlaybackSDK/PlayerUIView/PlaybackUIView.swift index 2b44ecd..894eb52 100644 --- a/Sources/PlaybackSDK/PlayerUIView/PlaybackUIView.swift +++ b/Sources/PlaybackSDK/PlayerUIView/PlaybackUIView.swift @@ -29,7 +29,7 @@ internal struct PlaybackUIView: View { @State private var hasFetchedVideoDetails = false /// The fetched video details of the entryIDs - @State private var videoDetails: [PlaybackResponseModel]? + @State private var videoDetails: [PlaybackVideoDetails]? /// Array of errors for fetching playlist details @State private var playlistErrors: [PlaybackAPIError]? /// Error of failed API call for loading video details @@ -44,8 +44,10 @@ internal struct PlaybackUIView: View { Initializes the `PlaybackUIView` with the provided list of entry ID and authorization token. - Parameters: - - entryId: A list of entry ID of the video to be played. - - authorizationToken: Optional authorization token if required to fetch the video details. + - entryIds: A list of entry ID of the video to be played. + - entryIDToPlay: (Optional) The first video Id to be played. If not provided, the first video in the entryIDs array will be played. + - authorizationToken: (Optional) Authorization token if required to fetch the video details. + - onErrors: Return a list of potential playback errors that may occur during the loading process for single entryId. */ internal init(entryIds: [String], entryIDToPlay: String?, authorizationToken: String?, onErrors: (([PlaybackAPIError]) -> Void)?) { self.entryIds = entryIds @@ -58,8 +60,9 @@ internal struct PlaybackUIView: View { Initializes the `PlaybackUIView` with the provided list of entry ID and authorization token. - Parameters: - - entryId: An entry ID of the video to be played. - - authorizationToken: Optional authorization token if required to fetch the video details. + - entryId: An entry ID of the video to be played. + - authorizationToken: Optional authorization token if required to fetch the video details. + - onError: Return potential playback errors that may occur during the loading process. */ internal init(entryId: String, authorizationToken: String?, onError: ((PlaybackAPIError) -> Void)?) { self.entryIds = [entryId] @@ -101,12 +104,16 @@ internal struct PlaybackUIView: View { */ private func loadHLSStream() { - //TO-DO Fetch all HLS urls from the entryID array PlaybackSDKManager.shared.loadAllHLSStream(forEntryIds: entryIds, andAuthorizationToken: authorizationToken) { result in switch result { case .success(let videoDetails): DispatchQueue.main.async { - self.videoDetails = videoDetails.0 + self.videoDetails = [] + for details in videoDetails.0 { + if let videoDetails = details.toVideoDetails() { + self.videoDetails?.append(videoDetails) + } + } self.playlistErrors = videoDetails.1 self.hasFetchedVideoDetails = true if (!(self.playlistErrors?.isEmpty ?? false)) {