diff --git a/Sources/PlaybackSDK/PlaybackSDKManager.swift b/Sources/PlaybackSDK/PlaybackSDKManager.swift index f73f76e..32b70db 100644 --- a/Sources/PlaybackSDK/PlaybackSDKManager.swift +++ b/Sources/PlaybackSDK/PlaybackSDKManager.swift @@ -8,6 +8,7 @@ import Foundation import Combine import SwiftUI +import BitmovinPlayer // Errors.swift @@ -176,7 +177,7 @@ public class PlaybackSDKManager { ) -> some View { PlaybackUIView( - entryId: [entryID], + entryId: entryID, authorizationToken: authorizationToken, onError: onError ) @@ -198,12 +199,14 @@ public class PlaybackSDKManager { */ public func loadPlaylist( entryIDs: [String], + entryIDToPlay: String? = nil, authorizationToken: String? = nil, onErrors: (([PlaybackAPIError]) -> Void)? ) -> some View { PlaybackUIView( - entryId: entryIDs, + entryIds: entryIDs, + entryIDToPlay: entryIDToPlay, authorizationToken: authorizationToken, onErrors: onErrors ) @@ -360,6 +363,32 @@ public class PlaybackSDKManager { .eraseToAnyPublisher() } } + + func createSource(from details: PlaybackResponseModel, authorizationToken: String?) -> Source? { + + guard let hlsURLString = details.media?.hls, 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.sourceDescription = details.description + 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) + 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 eeace36..cda08d9 100644 --- a/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerPlugin.swift +++ b/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerPlugin.swift @@ -20,6 +20,8 @@ public class BitmovinPlayerPlugin: VideoPlayerPlugin, ObservableObject { } } private var cancellables = Set() + private var authorizationToken: String? = nil + private var entryIDToPlay: String? // Required properties public let name: String @@ -52,7 +54,9 @@ public class BitmovinPlayerPlugin: VideoPlayerPlugin, ObservableObject { playerConfig.styleConfig.userInterfaceConfig = uiConfig } - public func playerView(videoDetails: [PlaybackResponseModel]) -> AnyView { + public func playerView(videoDetails: [PlaybackResponseModel], entryIDToPlay: String?, authorizationToken: String?) -> AnyView { + self.authorizationToken = authorizationToken + self.entryIDToPlay = entryIDToPlay // Create player based on player and analytics configurations // Check if player already loaded in order to avoid multiple pending player in memory if self.player == nil { @@ -64,6 +68,8 @@ public class BitmovinPlayerPlugin: VideoPlayerPlugin, ObservableObject { return AnyView( BitmovinPlayerView( videoDetails: videoDetails, + entryIDToPlay: entryIDToPlay, + authorizationToken: self.authorizationToken, player: player ) ) @@ -72,6 +78,8 @@ public class BitmovinPlayerPlugin: VideoPlayerPlugin, ObservableObject { return AnyView( BitmovinPlayerView( videoDetails: videoDetails, + entryIDToPlay: entryIDToPlay, + authorizationToken: self.authorizationToken, player: self.player! ) ) @@ -114,7 +122,7 @@ public class BitmovinPlayerPlugin: VideoPlayerPlugin, ObservableObject { let nextIndex = index + 1 if nextIndex < sources.count { let nextSource = sources[nextIndex] - player?.playlist.seek(source: nextSource, time: 0) + seekSource(to: nextSource) } } } @@ -125,7 +133,7 @@ public class BitmovinPlayerPlugin: VideoPlayerPlugin, ObservableObject { if let index = sources.firstIndex(where: { $0.isActive }) { if index > 0 { let prevSource = sources[(index) - 1] - player?.playlist.seek(source: prevSource, time: 0) + seekSource(to: prevSource) } } } @@ -133,24 +141,64 @@ public class BitmovinPlayerPlugin: VideoPlayerPlugin, ObservableObject { public func last() { if let lastSource = player?.playlist.sources.last { - player?.playlist.seek(source: lastSource, time: 0) + seekSource(to: lastSource) } } public func first() { if let firstSource = player?.playlist.sources.first { - player?.playlist.seek(source: firstSource, time: 0) + seekSource(to: firstSource) } } - public func seek(to entryId: String) -> Bool { + public func seek(to entryId: String, completion: @escaping (Bool) -> Void) { if let sources = player?.playlist.sources { if let index = sources.firstIndex(where: { $0.metadata?["entryId"] as? String == entryId }) { - player?.playlist.seek(source: sources[index], time: 0) - return true + seekSource(to: sources[index]) { success in + completion(success) + } + } else { + completion(false) + } + } else { + completion(false) + } + } + + private func seekSource(to source: Source, completion: ( (Bool) -> (Void))? = nil) { + if let sources = player?.playlist.sources { + if let index = sources.firstIndex(where: { $0 === source }) { + updateSource(for: sources[index]) { updatedSource in + if let updatedSource = updatedSource { + self.player?.playlist.remove(sourceAt: index) + self.player?.playlist.add(source: updatedSource, at: index) + self.player?.playlist.seek(source: updatedSource, time: 0) + completion?(true) + } else { + completion?(false) + } + } + } + } + } + + private func updateSource(for source: Source, completion: @escaping (Source?) -> Void) { + + let entryId = source.metadata?["entryId"] as? String + let authorizationToken = source.metadata?["authorizationToken"] as? String + + 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 .failure: + break + completion(nil) + } } } - return false } public func activeEntryId() -> String? { diff --git a/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerView.swift b/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerView.swift index d48f6a1..dae4735 100644 --- a/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerView.swift +++ b/Sources/PlaybackSDK/Player Plugin/BitMovinPlugin/BitmovinPlayerView.swift @@ -19,6 +19,8 @@ public struct BitmovinPlayerView: View { private let player: Player private var playerViewConfig = PlayerViewConfig() private var sources: [Source] = [] + private var entryIDToPlay: String? + private var authorizationToken: String? private var playlistConfig: PlaylistConfig? { if sources.isEmpty || sources.count == 1 { @@ -43,9 +45,11 @@ public struct BitmovinPlayerView: View { /// /// - 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], player: Player) { + public init(videoDetails: [PlaybackResponseModel], entryIDToPlay: String?, authorizationToken: String?, player: Player) { self.player = player + self.authorizationToken = authorizationToken + self.entryIDToPlay = entryIDToPlay playerViewConfig = PlayerViewConfig() @@ -63,7 +67,7 @@ public struct BitmovinPlayerView: View { /// /// - 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], playerConfig: PlayerConfig) { + public init(videoDetails: [PlaybackResponseModel], entryIDToPlay: String?, authorizationToken: String?, playerConfig: PlayerConfig) { let uiConfig = BitmovinUserInterfaceConfig() uiConfig.hideFirstFrame = true @@ -73,6 +77,8 @@ public struct BitmovinPlayerView: View { self.player = PlayerFactory.createPlayer( playerConfig: playerConfig ) + self.authorizationToken = authorizationToken + self.entryIDToPlay = entryIDToPlay sources = createPlaylist(from: videoDetails) @@ -94,6 +100,12 @@ public struct BitmovinPlayerView: View { if let playlistConfig = self.playlistConfig { // Multiple videos with playlist available so load player with playlistConfig player.load(playlistConfig: playlistConfig) + if let entryIDToPlay = self.entryIDToPlay { + if let index = player.playlist.sources.firstIndex(where: { $0.sourceConfig.metadata["entryId"] as? String == entryIDToPlay }) { + player.playlist.seek(source: sources[index], time: .zero) + player.seek(time: .zero) + } + } } else if let sourceConfig = self.sourceConfig { // Single video available so load player with sourceConfig player.load(sourceConfig: sourceConfig) @@ -108,34 +120,13 @@ public struct BitmovinPlayerView: View { var sources: [Source] = [] for details in videoDetails { - if let videoSource = createSource(from: details) { + if let videoSource = PlaybackSDKManager.shared.createSource(from: details, authorizationToken: self.authorizationToken) { sources.append(videoSource) } } return sources } - - func createSource(from details: PlaybackResponseModel) -> Source? { - - guard let hlsURLString = details.media?.hls, let hlsURL = URL(string: hlsURLString) else { - return nil - } - - let sourceConfig = SourceConfig(url: hlsURL, type: .hls) - sourceConfig.title = details.name - sourceConfig.posterSource = details.coverImg?._360 - sourceConfig.sourceDescription = details.description - 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) - } - } - return SourceFactory.createSource(from: sourceConfig) - } func setupRemoteTransportControls() { // Get the shared MPRemoteCommandCenter diff --git a/Sources/PlaybackSDK/Player Plugin/VideoPlayerPlugin.swift b/Sources/PlaybackSDK/Player Plugin/VideoPlayerPlugin.swift index b920635..abaf75f 100644 --- a/Sources/PlaybackSDK/Player Plugin/VideoPlayerPlugin.swift +++ b/Sources/PlaybackSDK/Player Plugin/VideoPlayerPlugin.swift @@ -21,7 +21,7 @@ public protocol VideoPlayerPlugin: AnyObject { // TODO: add event /// func handleEvent(event: BitmovinPlayerCore.PlayerEvent) - func playerView(videoDetails: [PlaybackResponseModel]) -> AnyView + func playerView(videoDetails: [PlaybackResponseModel], entryIDToPlay: String?, authorizationToken: String?) -> AnyView func play() @@ -35,7 +35,7 @@ public protocol VideoPlayerPlugin: AnyObject { func first() - func seek(to entryId: String) -> Bool + func seek(to entryId: String, completion: @escaping (Bool) -> Void) func activeEntryId() -> String? diff --git a/Sources/PlaybackSDK/PlayerUIView/PlaybackUIView.swift b/Sources/PlaybackSDK/PlayerUIView/PlaybackUIView.swift index 1b2bc72..2b44ecd 100644 --- a/Sources/PlaybackSDK/PlayerUIView/PlaybackUIView.swift +++ b/Sources/PlaybackSDK/PlayerUIView/PlaybackUIView.swift @@ -16,6 +16,9 @@ internal struct PlaybackUIView: View { /// The entry ID or a list of the videos to be played. private var entryIds: [String] + /// The entryID to play at the beginning + private var entryIDToPlay: String? + /// Optional authorization token if required to fetch the video details. private var authorizationToken: String? @@ -44,8 +47,9 @@ internal struct PlaybackUIView: View { - entryId: A list of entry ID of the video to be played. - authorizationToken: Optional authorization token if required to fetch the video details. */ - internal init(entryId: [String], authorizationToken: String?, onErrors: (([PlaybackAPIError]) -> Void)?) { - self.entryIds = entryId + internal init(entryIds: [String], entryIDToPlay: String?, authorizationToken: String?, onErrors: (([PlaybackAPIError]) -> Void)?) { + self.entryIds = entryIds + self.entryIDToPlay = entryIDToPlay ?? entryIds.first self.authorizationToken = authorizationToken self.onErrors = onErrors } @@ -54,11 +58,11 @@ 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. + - entryId: An entry ID of the video to be played. - authorizationToken: Optional authorization token if required to fetch the video details. */ - internal init(entryId: [String], authorizationToken: String?, onError: ((PlaybackAPIError) -> Void)?) { - self.entryIds = entryId + internal init(entryId: String, authorizationToken: String?, onError: ((PlaybackAPIError) -> Void)?) { + self.entryIds = [entryId] self.authorizationToken = authorizationToken self.onError = onError } @@ -74,7 +78,7 @@ internal struct PlaybackUIView: View { } else { if let videoDetails = videoDetails { if let plugin = pluginManager.selectedPlugin { - plugin.playerView(videoDetails: videoDetails) + plugin.playerView(videoDetails: videoDetails, entryIDToPlay: entryIDToPlay, authorizationToken: authorizationToken) } else { ErrorUIView(errorMessage: "No plugin selected") .background(Color.white)