Skip to content

Commit

Permalink
Merge pull request #8 from NullIsOne/improve-option-buttons-management
Browse files Browse the repository at this point in the history
Improve option buttons management
  • Loading branch information
NullIsOne authored Apr 2, 2024
2 parents 520b4d4 + c31f4b6 commit 791da3f
Show file tree
Hide file tree
Showing 14 changed files with 277 additions and 26 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@
- live stream announce view
- collecting of analytic events with playback data for Kinescope dashboard
- textField in example project to allow user to input videoId

- ability to manage options menu on player view
47 changes: 47 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,53 @@ All components of `KinescopePlayerView` is partially customisable. You can chang

All properties of `KinescopePlayerViewConfiguration` have default values included in SDK, so customisation is optional.

## Managing of options menu

Options menu is a set of buttons with different actions like AirPlay, Picture in Picture, etc. You can manage this menu by adding or removing some options.

By default, options menu is collapsed, but you can show it by tapping on the options button with dots in the bottom right corner of the player view.

### Custom options

To add some custom option you can use method `addOption` with `KinescopePlayerOption` enum value.

For example

```swift
if let shareIcon = UIImage(systemName: "square.and.arrow.up") {
player?.addCustomPlayerOption(with: CustomPlayerOption.share,
and: shareIcon)
}
```

To handle tap on custom option you should implement `KinescopePlayerDelegate` protocol and use method `player(didSelect option: KinescopePlayerOption)`.

```swift
func player(didSelectCustomOptionWith optionId: AnyHashable, anchoredAt view: UIView) {
guard let option = optionId as? CustomPlayerOption else {
return
}

switch option {
case .share:
// your code here
}
}
```
AnchoredView is button which were tapped. You can use it to show popover or action sheet. It's required on iPad applications.

### Hide/disable options

To hide some built-in options which you do not want you can use method `disableOptions` with array of options you want to hide.

For example, to hide AirPlay option use this code:
```swift
player?.disableOptions([.airPlay])
```
All built-in options are listed in `KinescopePlayerOption` enum.

Using `disableOptions` also can disable custom option if id of this option is equal to previously added option.

## Configuration parameters

### gravity
Expand Down
36 changes: 36 additions & 0 deletions Example/KinescopeExample/Flows/VideoViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ import UIKit
import KinescopeSDK

final class VideoViewController: UIViewController {

// MARK: - Nested Types

private enum CustomPlayerOption: String {
case share
}

// MARK: - IBOutlets

Expand Down Expand Up @@ -40,6 +46,15 @@ final class VideoViewController: UIViewController {
PipManager.shared.closePipIfNeeded(with: videoId)

player = KinescopeVideoPlayer(config: .init(videoId: videoId))

if #available(iOS 13.0, *) {
if let shareIcon = UIImage(systemName: "square.and.arrow.up")?.withRenderingMode(.alwaysTemplate) {
player?.addCustomPlayerOption(with: CustomPlayerOption.share, and: shareIcon)
}
}
player?.disableOptions([.airPlay])

player?.setDelegate(delegate: self)
player?.attach(view: playerView)
player?.play()
player?.pipDelegate = PipManager.shared
Expand All @@ -53,3 +68,24 @@ extension VideoViewController: UINavigationControllerDelegate {
return self.supportedInterfaceOrientations
}
}

extension VideoViewController: KinescopeVideoPlayerDelegate {

func player(didSelectCustomOptionWith optionId: AnyHashable, anchoredAt view: UIView) {
guard let option = optionId as? CustomPlayerOption else {
return
}

switch option {
case .share:
let items = [player?.config.shareLink]
let activityViewController = UIActivityViewController(activityItems: items as [Any],
applicationActivities: nil)
let presentationController = activityViewController.presentationController as? UIPopoverPresentationController
presentationController?.sourceView = view
presentationController?.permittedArrowDirections = .down
present(activityViewController, animated: true)
}
}

}
5 changes: 5 additions & 0 deletions Sources/KinescopeSDK/Models/KinescopePlayerConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ public struct KinescopePlayerConfig {

/// If value is `true` show video in infinite loop.
public let looped: Bool

/// Default link to share video and play it on web.
public var shareLink: URL? {
URL(string: "https://kinescope.io/\(videoId)")
}

/// - parameter videoId: Id of concrete video. For example from [GET Videos list](https://documenter.getpostman.com/view/10589901/TVCcXpNM)
/// - parameter looped: If value is `true` show video in infinite loop. By default is `false`
Expand Down
90 changes: 89 additions & 1 deletion Sources/KinescopeSDK/Models/Options/KinescopePlayerOption.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
// Created by Никита Коробейников on 01.04.2021.
//

import UIKit

/// Options available in player control panel
public enum KinescopePlayerOption: String {
public enum KinescopePlayerOption {

/// Option to expand control panel and show all available options
case more
Expand All @@ -24,4 +26,90 @@ public enum KinescopePlayerOption: String {
case subtitles
/// Option to enter in Picture in Picture mode, availalble only if device supported PiP
case pip
/// Custom option to perform any action
/// - Parameters:
/// - id: Uniq identifier to distinguish selection of different button. Maybe `String`, `Int`, `UUID` or other `Hashable`.
/// - icon: Image to represent option in menu
case custom(id: AnyHashable, icon: UIImage)

// MARK: - Appearance

var icon: UIImage {
switch self {
case .more:
return UIImage.image(named: "more")
case .fullscreen:
return UIImage.image(named: "fullscreen")
case .settings:
return UIImage.image(named: "settings")
case .attachments:
return UIImage.image(named: "attachments")
case .download:
return UIImage.image(named: "download")
case .airPlay:
return UIImage.image(named: "airPlay")
case .subtitles:
return UIImage.image(named: "subtitles")
case .pip:
return UIImage.image(named: "pip")
case .custom(_, let icon):
return icon
}
}

var iconSelected: UIImage? {
switch self {
case .subtitles:
return UIImage.image(named: "subtitlesSelected")
case .airPlay:
return UIImage.image(named: "airPlayActive")
case .custom(_, let icon):
return icon
default:
return nil
}
}

// MARK: - Id:

var optionId: AnyHashable? {
switch self {
case .custom(let id, _):
return id
default:
return nil
}
}

}

// MARK: - Equatable

extension KinescopePlayerOption: Equatable {

public static func ==(lhs: Self, rhs: Self) -> Bool {
switch (lhs, rhs) {
case (.more, .more):
return true
case (.fullscreen, .fullscreen):
return true
case (.settings, .settings):
return true
case (.attachments, .attachments):
return true
case (.download, .download):
return true
case (.airPlay, .airPlay):
return true
case (.subtitles, .subtitles):
return true
case (.pip, .pip):
return true
case (.custom(let lId, _), .custom(let rId, _)):
return lId == rId
default:
return false
}
}

}
18 changes: 16 additions & 2 deletions Sources/KinescopeSDK/Player/KinescopeVideoPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public class KinescopeVideoPlayer: KinescopePlayer, KinescopePlayerBody, Fullscr

// MARK: - Private Properties

private let config: KinescopePlayerConfig
public let config: KinescopePlayerConfig
private let dependencies: KinescopePlayerDependencies

private var analyticStorage: InnerEventsDataStorage & InnerEventsDataInputs = InMemoryInnerEventsDataStorage()
Expand Down Expand Up @@ -89,6 +89,8 @@ public class KinescopeVideoPlayer: KinescopePlayer, KinescopePlayerBody, Fullscr
return threshold < Constants.maxPlaybackThreshold ? threshold : Constants.maxPlaybackThreshold
}

private var customOptions = [KinescopePlayerOption]()
private var disabledOptions = [KinescopePlayerOption]()
private var options = [KinescopePlayerOption]()

private var playbackObserverFactory: (any Factory)?
Expand Down Expand Up @@ -213,6 +215,14 @@ public class KinescopeVideoPlayer: KinescopePlayer, KinescopePlayerBody, Fullscr
public func setDelegate(delegate: KinescopeVideoPlayerDelegate) {
self.delegate = delegate
}

public func addCustomPlayerOption(with id: AnyHashable, and icon: UIImage) {
customOptions.append(.custom(id: id, icon: icon))
}

public func disableOptions(_ options: [KinescopePlayerOption]) {
disabledOptions = options
}
}

// MARK: - Private
Expand Down Expand Up @@ -260,7 +270,7 @@ private extension KinescopeVideoPlayer {
if video.hasSubtitles {
options.insert(.subtitles, at: 0)
}

options = (customOptions + options).filter { !disabledOptions.contains($0) }
self.options = options
return options
}
Expand Down Expand Up @@ -665,4 +675,8 @@ extension KinescopeVideoPlayer: KinescopePlayerViewDelegate {
level: KinescopeLoggerLevel.player)
}

func didSelect(option: AnyHashable, optionView: UIView) {
delegate?.player(didSelectCustomOptionWith: option, anchoredAt: optionView)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public protocol KinescopeVideoPlayerDelegate: AnyObject {
func player(didFastBackwardTo time: TimeInterval)
/// Triggered on quality change
func player(changedQualityTo quality: String)
/// Triggered on custom option button selected in options menu
func player(didSelectCustomOptionWith optionId: AnyHashable, anchoredAt view: UIView)
}

public extension KinescopeVideoPlayerDelegate {
Expand Down
15 changes: 15 additions & 0 deletions Sources/KinescopeSDK/Protocols/KinescopePlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import AVKit

/// Control protocol for player
public protocol KinescopePlayer {

/// Current configuration of player.
/// Contains `videoId` and playing strategy.
var config: KinescopePlayerConfig { get }

/// Delegate of Picture in Picture controller
var pipDelegate: AVPictureInPictureControllerDelegate? { get set }
Expand Down Expand Up @@ -41,4 +45,15 @@ public protocol KinescopePlayer {
/// Set delegate object
/// - Parameter delegate: delegate for player events
func setDelegate(delegate: KinescopeVideoPlayerDelegate)

/// Add custom options to bottom menu
/// - Parameters:
/// - id: Uniq identifier of option
/// - icon: Image to represent option in menu
func addCustomPlayerOption(with id: AnyHashable, and icon: UIImage)

/// Hides options from bottom menu
/// - Parameters:
/// - options: Array of options to ignore.
func disableOptions(_ options: [KinescopePlayerOption])
}
2 changes: 2 additions & 0 deletions Sources/KinescopeSDK/View/Player/KinescopePlayerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,8 @@ extension KinescopePlayerView: PlayerControlOutput {
case .pip:
let isPipActive = pipController?.isPictureInPictureActive ?? false
isPipActive ? pipController?.stopPictureInPicture() : pipController?.startPictureInPicture()
case .custom(let id, _):
delegate?.didSelect(option: id, optionView: controlPanel?.getCustomOptionView(by: id) ?? self)
default:
break
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ protocol KinescopePlayerViewDelegate: AnyObject {
func didSelectAttachment(with id: String)
func didSelectDownloadAll(for title: String)
func didSelect(subtitles: String)
func didSelect(option: AnyHashable, optionView: UIView)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
//
// AirPlayOptionButton.swift
// AirPlayOptionControl.swift
// KinescopeSDK
//
// Created by Никита Гагаринов on 13.04.2021.
//

import AVKit
import MediaPlayer

final class AirPlayOptionButton: UIButton {
final class AirPlayOptionControl: UIControl {

// MARK: - Initialization

Expand All @@ -33,14 +34,25 @@ final class AirPlayOptionButton: UIButton {

// MARK: - Private Methods

private extension AirPlayOptionButton {
private extension AirPlayOptionControl {

func setupInitialState() {
let volumeView = MPVolumeView()
volumeView.showsVolumeSlider = false
volumeView.setRouteButtonImage(UIImage.image(named: getImageName(for: volumeView)), for: .normal)
addSubview(volumeView)
stretch(view: volumeView)
let systemView: UIView
if #available(iOS 11.0, *) {
let routePickerView = AVRoutePickerView()
if #available(iOS 13.0, *) {
routePickerView.prioritizesVideoDevices = true
}
systemView = routePickerView
} else {
let volumeView = MPVolumeView(frame: .zero)
volumeView.showsVolumeSlider = false
volumeView.setRouteButtonImage(UIImage.image(named: getImageName(for: volumeView)), for: .normal)
systemView = volumeView
}

addSubview(systemView)
stretch(view: systemView)
}

func getImageName(for volumeView: MPVolumeView) -> String {
Expand Down
4 changes: 2 additions & 2 deletions Sources/KinescopeSDK/View/Player/Layout/OptionButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ final class OptionButton: UIButton {
private extension OptionButton {

func setupInitialState() {
setImage(UIImage.image(named: option.rawValue), for: .normal)
setImage(UIImage.image(named: option.rawValue + "Selected"), for: .selected)
setImage(option.icon, for: .normal)
setImage(option.iconSelected, for: .selected)
}

}
Loading

0 comments on commit 791da3f

Please sign in to comment.