Skip to content

Commit

Permalink
CORE-4594 Playlist API integration
Browse files Browse the repository at this point in the history
- Adding new playlist logic fetching video details every playing video
- Load playlist passing the starting entryId (entryIDToPlay)
- Seek entryId with completion
  • Loading branch information
StefanoStream committed Nov 13, 2024
1 parent 0b6e45b commit c576a85
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 43 deletions.
33 changes: 31 additions & 2 deletions Sources/PlaybackSDK/PlaybackSDKManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Foundation
import Combine
import SwiftUI
import BitmovinPlayer

// Errors.swift

Expand Down Expand Up @@ -176,7 +177,7 @@ public class PlaybackSDKManager {
) -> some View {

PlaybackUIView(
entryId: [entryID],
entryId: entryID,
authorizationToken: authorizationToken,
onError: onError
)
Expand All @@ -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
)
Expand Down Expand Up @@ -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)
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public class BitmovinPlayerPlugin: VideoPlayerPlugin, ObservableObject {
}
}
private var cancellables = Set<AnyCancellable>()
private var authorizationToken: String? = nil
private var entryIDToPlay: String?

// Required properties
public let name: String
Expand Down Expand Up @@ -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 {
Expand All @@ -64,6 +68,8 @@ public class BitmovinPlayerPlugin: VideoPlayerPlugin, ObservableObject {
return AnyView(
BitmovinPlayerView(
videoDetails: videoDetails,
entryIDToPlay: entryIDToPlay,
authorizationToken: self.authorizationToken,
player: player
)
)
Expand All @@ -72,6 +78,8 @@ public class BitmovinPlayerPlugin: VideoPlayerPlugin, ObservableObject {
return AnyView(
BitmovinPlayerView(
videoDetails: videoDetails,
entryIDToPlay: entryIDToPlay,
authorizationToken: self.authorizationToken,
player: self.player!
)
)
Expand Down Expand Up @@ -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)
}
}
}
Expand All @@ -125,32 +133,72 @@ 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)
}
}
}
}

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

Expand All @@ -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
Expand All @@ -73,6 +77,8 @@ public struct BitmovinPlayerView: View {
self.player = PlayerFactory.createPlayer(
playerConfig: playerConfig
)
self.authorizationToken = authorizationToken
self.entryIDToPlay = entryIDToPlay

sources = createPlaylist(from: videoDetails)

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

Expand All @@ -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?

Expand Down
16 changes: 10 additions & 6 deletions Sources/PlaybackSDK/PlayerUIView/PlaybackUIView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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)
Expand Down

0 comments on commit c576a85

Please sign in to comment.