Skip to content

Commit

Permalink
CORE-4594 Playlist API integration
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
StefanoStream committed Dec 2, 2024
1 parent 7a2b39f commit 0029ac4
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<Result<PlaybackResponseModel, Error>, Error>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 28 additions & 0 deletions Sources/PlaybackSDK/Playback API/PlaybackVideoDetails.swift
Original file line number Diff line number Diff line change
@@ -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
61 changes: 34 additions & 27 deletions Sources/PlaybackSDK/PlaybackSDKManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Error>) -> Void) {
guard !apiKey.isEmpty else {
completion(.failure(SDKError.initializationError))
Expand All @@ -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,
Expand All @@ -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],
Expand Down Expand Up @@ -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)
}

}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {

Expand Down
5 changes: 1 addition & 4 deletions Sources/PlaybackSDK/Player Plugin/VideoPlayerPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
21 changes: 14 additions & 7 deletions Sources/PlaybackSDK/PlayerUIView/PlaybackUIView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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]
Expand Down Expand Up @@ -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)) {
Expand Down

0 comments on commit 0029ac4

Please sign in to comment.