Skip to content

Commit

Permalink
Merge pull request #13 from kinescope/hotfix/overlay-independent-retry
Browse files Browse the repository at this point in the history
Fix live-translation in looped strategy with partially disabled UI
  • Loading branch information
NullIsOne authored Jun 27, 2024
2 parents 8a52a34 + 3f459dc commit 3054b93
Show file tree
Hide file tree
Showing 17 changed files with 218 additions and 30 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
## 0.2.2

### Added

- Additional parameter for `KinescopePlayerConfig` to set up repeating attempts count for video loading

### Changed

- Indication of any pauses during playing of video with loading indicator

### Fixed

- Observation of preview/loading/playing status for player config with `looped` enabled
- Restoration of playback after network or stream lost

## 0.2.1

### Fixed bugs
Expand Down
2 changes: 2 additions & 0 deletions Example/KinescopeExample/Flows/EnterViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ final class EnterViewController: UIViewController {
// MARK: - IBOutlets

@IBOutlet weak var field: UITextField!
@IBOutlet weak var uiSwitch: UISwitch!

// MARK: - Private properties

Expand Down Expand Up @@ -39,6 +40,7 @@ final class EnterViewController: UIViewController {
return
}
destination.videoId = videoId
destination.uiEnabled = uiSwitch.isOn
}

// MARK: - Actions
Expand Down
21 changes: 18 additions & 3 deletions Example/KinescopeExample/Flows/VideoViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ final class VideoViewController: UIViewController {
// MARK: - Public Properties

var videoId: String = ""
var uiEnabled: Bool = true

// MARK: - Appearance

Expand All @@ -41,19 +42,33 @@ final class VideoViewController: UIViewController {

navigationController?.delegate = self

playerView.setLayout(with: .accentTimeLineAndPlayButton(with: .orange))
if uiEnabled {
playerView.setLayout(with: .accentTimeLineAndPlayButton(with: .orange))
} else {
playerView.setLayout(with: .builder()
.setGravity(.resizeAspect)
.setOverlay(nil)
.setControlPanel(nil)
.setShadowOverlay(nil)
.build()
)
}

PipManager.shared.closePipIfNeeded(with: videoId)

let repeatingMode: RepeatingMode = uiEnabled ? .default : .infinite(interval: .seconds(5))

player = KinescopeVideoPlayer(config: .init(videoId: videoId))
player = KinescopeVideoPlayer(config: .init(videoId: videoId,
looped: !uiEnabled,
repeatingMode: repeatingMode))

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()
Expand Down
16 changes: 15 additions & 1 deletion Example/KinescopeExample/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22685"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
Expand Down Expand Up @@ -47,6 +47,15 @@
<action selector="didTapPlay:" destination="8D4-Vc-teG" eventType="touchUpInside" id="294-NJ-Nxf"/>
</connections>
</button>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" toolTip="UIenabled" on="YES" title="UIEnabled" translatesAutoresizingMaskIntoConstraints="NO" id="UvK-HX-Vij">
<rect key="frame" x="333" y="119" width="51" height="31"/>
</switch>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Player UI Enabled" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GBg-RZ-G5U">
<rect key="frame" x="32" y="124" width="133" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="B0s-tH-pXr"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
Expand All @@ -55,12 +64,17 @@
<constraint firstItem="wFE-5t-BHg" firstAttribute="leading" secondItem="824-rl-9c7" secondAttribute="trailing" constant="16" id="3UH-DD-ebf"/>
<constraint firstItem="B0s-tH-pXr" firstAttribute="trailing" secondItem="wFE-5t-BHg" secondAttribute="trailing" constant="32" id="Q0Q-Z3-4Qa"/>
<constraint firstItem="824-rl-9c7" firstAttribute="centerY" secondItem="B0s-tH-pXr" secondAttribute="centerY" id="RUx-cK-SEW"/>
<constraint firstItem="GBg-RZ-G5U" firstAttribute="top" secondItem="B0s-tH-pXr" secondAttribute="top" constant="32" id="czb-OU-4cc"/>
<constraint firstItem="GBg-RZ-G5U" firstAttribute="centerY" secondItem="UvK-HX-Vij" secondAttribute="centerY" id="dO0-Jh-lBc"/>
<constraint firstItem="824-rl-9c7" firstAttribute="leading" secondItem="B0s-tH-pXr" secondAttribute="leading" constant="32" id="hlL-b6-GO7"/>
<constraint firstItem="B0s-tH-pXr" firstAttribute="trailing" secondItem="UvK-HX-Vij" secondAttribute="trailing" constant="32" id="jRX-CV-bMY"/>
<constraint firstItem="GBg-RZ-G5U" firstAttribute="leading" secondItem="B0s-tH-pXr" secondAttribute="leading" constant="32" id="upc-AS-fSj"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="ktQ-3P-CGU"/>
<connections>
<outlet property="field" destination="824-rl-9c7" id="xA8-F2-XZs"/>
<outlet property="uiSwitch" destination="UvK-HX-Vij" id="fxt-42-DgF"/>
<segue destination="DPB-hC-LdC" kind="show" identifier="Player" id="Ccv-TT-Kwb"/>
</connections>
</viewController>
Expand Down
4 changes: 2 additions & 2 deletions Example/Podfile.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
PODS:
- KinescopeSDK (0.2.1):
- KinescopeSDK (0.2.2):
- M3U8Kit (~> 1.0)
- SwiftProtobuf (= 1.26.0)
- M3U8Kit (1.0.2)
Expand All @@ -18,7 +18,7 @@ EXTERNAL SOURCES:
:path: "../"

SPEC CHECKSUMS:
KinescopeSDK: 46658fde8178bf17feec2c8b24ce2f74ea751be9
KinescopeSDK: ea5459195c0d371a45e75b0b09c25e145a1e2fad
M3U8Kit: 6a0d55ba2e62e146f34f53276d7c3aa78ed4fc00
SwiftProtobuf: 5e8349171e7c2f88f5b9e683cb3cb79d1dc780b3

Expand Down
2 changes: 1 addition & 1 deletion KinescopeSDK.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "KinescopeSDK"
s.version = "0.2.1"
s.version = "0.2.2"
s.summary = "Library to help you include Kinescope player into your mobile iOS application"
s.homepage = "https://github.com/kinescope/ios-kinescope-player"
s.license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Kinescope iOS SDK

![Pub Version](https://img.shields.io/badge/version-0.2.1-orange)
![Pub Version](https://img.shields.io/badge/version-0.2.2-orange)

This framework created to help you include [Kinescope](https://kinescope.io/) player into your mobile iOS application.

Expand Down
2 changes: 2 additions & 0 deletions Sources/KinescopeSDK/Bags/KVO/KVOSubKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import Foundation

enum KVOSubKey {
case playerStatus
case playerItem
case playerItemBufferEmpty
case playerItemStatus
case playerTimeControlStatus
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ enum NotificationSubKey {
case appDidEnterBackground
case deviceOrientationChanged
case itemDidPlayToEnd
case itemFailedToPlayToEndTime

var notificationName: NSNotification.Name {
switch self {
Expand All @@ -26,6 +27,8 @@ enum NotificationSubKey {
return UIDevice.orientationDidChangeNotification
case .itemDidPlayToEnd:
return AVPlayerItem.didPlayToEndTimeNotification
case .itemFailedToPlayToEndTime:
return AVPlayerItem.failedToPlayToEndTimeNotification
}
}

Expand Down
7 changes: 6 additions & 1 deletion Sources/KinescopeSDK/Models/KinescopePlayerConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,21 @@ public struct KinescopePlayerConfig {
/// If value is `true` show video in infinite loop.
public let looped: Bool

/// Repeating mode for player
public let repeatingMode: RepeatingMode

/// 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`
public init(videoId: String, looped: Bool = false) {
/// - parameter repeatingMode: Mode which will be used to repeat failed requests.
public init(videoId: String, looped: Bool = false, repeatingMode: RepeatingMode = .default) {
self.videoId = videoId
self.looped = looped
self.repeatingMode = repeatingMode
}

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

private lazy var notificationsBag = NotificationsBag(observer: self)

@Repeating(executionQueue: .main, attemptsLimit: 10, intervalSeconds: 5)
@Repeating(executionQueue: .main, mode: .default)
private var playRepeater

private(set) lazy var strategy: PlayingStrategy = {
Expand Down Expand Up @@ -122,8 +122,10 @@ public class KinescopeVideoPlayer: KinescopePlayer, KinescopePlayerBody, Fullscr

// MARK: - Lifecycle

init(config: KinescopePlayerConfig, dependencies: KinescopePlayerDependencies) {
init(config: KinescopePlayerConfig,
dependencies: KinescopePlayerDependencies) {
self.dependencies = dependencies
self._playRepeater = Repeating(executionQueue: .main, mode: config.repeatingMode)
self.config = config
playRepeater = .init(title: "play") { [weak self] in self?.play() }
addNotofications()
Expand All @@ -138,7 +140,8 @@ public class KinescopeVideoPlayer: KinescopePlayer, KinescopePlayerBody, Fullscr
// MARK: - KinescopePlayer

public required convenience init(config: KinescopePlayerConfig) {
self.init(config: config, dependencies: KinescopeVideoPlayerDependencies())
self.init(config: config,
dependencies: KinescopeVideoPlayerDependencies())
self.configureAnalytic()
}

Expand Down Expand Up @@ -185,6 +188,8 @@ public class KinescopeVideoPlayer: KinescopePlayer, KinescopePlayerBody, Fullscr
observePlaybackTime()
addPlayerTimeControlStatusObserver()
addPlayerStatusObserver()
addPlayerItemObserver()
addPlayerStatusObserver()
}

public func detach(view: KinescopePlayerView) {
Expand All @@ -201,16 +206,12 @@ public class KinescopeVideoPlayer: KinescopePlayer, KinescopePlayerBody, Fullscr

savedTime = strategy.player.currentTime()

kvoBag.removeObserver(for: .playerItemStatus)

if let item = quality.makeItem(with: drmHandler) {
strategy.bind(item: item)
}

// changing quality
strategy.player.currentItem?.preferredPeakBitRate = quality.preferredMaxBitRate

addPlayerItemStatusObserver()
}

public func setDelegate(delegate: KinescopeVideoPlayerDelegate) {
Expand Down Expand Up @@ -320,6 +321,24 @@ private extension KinescopeVideoPlayer {
repeater: $playRepeater)
kvoBag.addObserver(for: .playerStatus, using: .init(wrappedFactory: observerFactory))
}

func addPlayerItemObserver() {
let observerFactory = CurrentItemObserver(playerBody: self, currentItemChanged: {
[weak self] item in
self?.kvoBag.removeObserver(for: .playerItemStatus)
self?.kvoBag.removeObserver(for: .playerItemBufferEmpty)
if let item {
self?.addPlayerItemStatusObserver()
self?.addPlayerItemBufferIsEmptyObserver()
}
})
kvoBag.addObserver(for: .playerItem, using: .init(wrappedFactory: observerFactory))
}

func addPlayerItemBufferIsEmptyObserver() {
let observerFactory = EmptyBufferObserver(playerBody: self, repeater: $playRepeater)
kvoBag.addObserver(for: .playerItemBufferEmpty, using: .init(wrappedFactory: observerFactory))
}

func addPlayerItemStatusObserver() {
let observerFactory = CurrentItemStatusObserver(playerBody: self,
Expand Down Expand Up @@ -356,6 +375,8 @@ private extension KinescopeVideoPlayer {
using: .init(selector: #selector(changeOrientation)))
notificationsBag.addObserver(for: .itemDidPlayToEnd,
using: .init(selector: #selector(itemDidPlayToEnd)))
notificationsBag.addObserver(for: .itemFailedToPlayToEndTime,
using: .init(selector: #selector(itemFailedToPlayeToEndTime)))
}

func configureAnalytic() {
Expand Down Expand Up @@ -423,6 +444,11 @@ private extension KinescopeVideoPlayer {
analytic?.send(event: .end)
}

@objc func itemFailedToPlayeToEndTime(_ notification: Notification) {
let error = notification.userInfo?[AVPlayerItemFailedToPlayToEndTimeErrorKey] as? Error
Kinescope.shared.logger?.log(error: error, level: KinescopeLoggerLevel.player)
}

func restoreView() {
view?.showOverlay(isOverlayed)
isPlaying ? play() : pause()
Expand Down
38 changes: 38 additions & 0 deletions Sources/KinescopeSDK/Player/Observers/CurrentItemObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// CurrentItemObserver.swift
// KinescopeSDK
//
// Created by Nikita Korobeinikov on 16.06.2024.
//

import Foundation
import AVFoundation

final class CurrentItemObserver: KVOObserverFactory {

private weak var playerBody: KinescopePlayerBody?

private var currentItemChanged: (AVPlayerItem?) -> Void

init(playerBody: KinescopePlayerBody,
currentItemChanged: @escaping (AVPlayerItem?) -> Void) {
self.playerBody = playerBody
self.currentItemChanged = currentItemChanged
}

func provide() -> NSKeyValueObservation? {
playerBody?.strategy.player.observe(
\.currentItem,
options: [.new, .old],
changeHandler: { [weak self] item, _ in
self?.currentItemChanged(item.currentItem)

Kinescope.shared.logger?.log(
message: "AVPlayer.CurrentItem – \(item.currentItem.debugDescription)",
level: KinescopeLoggerLevel.player
)
}
)
}

}
41 changes: 41 additions & 0 deletions Sources/KinescopeSDK/Player/Observers/EmptyBufferObserver.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// EmptyBufferObserver.swift
// KinescopeSDK
//
// Created by Nikita Korobeinikov on 24.06.2024.
//

import Foundation
import AVFoundation

final class EmptyBufferObserver: KVOObserverFactory {

private weak var playerBody: KinescopePlayerBody?
private weak var repeater: Repeater?

init(playerBody: KinescopePlayerBody,
repeater: Repeater) {
self.repeater = repeater
self.playerBody = playerBody
}

func provide() -> NSKeyValueObservation? {
playerBody?.strategy.player.currentItem?.observe(
\.isPlaybackBufferEmpty,
options: [.new, .old],
changeHandler: { [weak self] item, value in
let isBufferEmpty = value.newValue ?? item.isPlaybackBufferEmpty
if isBufferEmpty {
self?.repeater?.start()
}

Kinescope.shared.logger?.log(
message: "AVPlayer.isPlaybackBufferEmpty – \(isBufferEmpty)",
level: KinescopeLoggerLevel.player
)
}
)
}

}

Loading

0 comments on commit 3054b93

Please sign in to comment.