Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/danger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Danger
uses: docker://ghcr.io/danger/danger-swift-with-swiftlint:3.12.3
uses: docker://ghcr.io/danger/danger-swift-with-swiftlint:3.15.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4 changes: 3 additions & 1 deletion .github/workflows/swift-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ jobs:
with:
xcode-version: '${{ matrix.ios.xcode }}'
- name: Build and Test (iOS ${{ matrix.ios.version }})
run: xcodebuild build -scheme GreedyKit -destination 'platform=iOS Simulator,OS=${{ matrix.ios.version }},name=${{ matrix.ios.device }}' test | xcbeautify
run: |
set -o pipefail
xcodebuild build -scheme GreedyKit -destination 'platform=iOS Simulator,OS=${{ matrix.ios.version }},name=${{ matrix.ios.device }}' test | xcbeautify
1 change: 1 addition & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ opt_in_rules:
- comment_spacing
- force_unwrapping
- sorted_imports
- void_function_in_ternary

line_length: 120
warning_threshold: 3
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ let package = Package(
),
.testTarget(
name: "GreedyKitTests",
dependencies: ["GreedyKit"]
dependencies: ["GreedyKit"],
resources: [
.process("Resources")
]
)
]
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// VideoRenderActor.swift
// VideoRenderer.swift
// GreedyKit
//
// Created by Igor Belov on 30.06.2025.
Expand All @@ -8,7 +8,7 @@
import AVFoundation
import CoreImage

final actor VideoRenderActor {
final actor VideoRenderer: VideoRendererProtocol {

private lazy var sampleBufferFactory = SampleBufferFactory()
private lazy var videoOutput = AVPlayerItemVideoOutput(
Expand Down
2 changes: 1 addition & 1 deletion Sources/GreedyKit/Core/Utils/BackedRenderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import AVFoundation
import UIKit

final class BackedRenderView: UIView {
final class BackedRenderView: UIView, RenderViewProtocol {

// MARK: - Internal API

Expand Down
4 changes: 2 additions & 2 deletions Sources/GreedyKit/Extensions/ConcurrencyShims.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

import AVFoundation

/// Safe because after attachment to an `AVPlayerItem`, we only access the output's
/// read-only methods from a single actor.
/// Safe because after attachment to an `AVPlayerItem`,
/// we only access the output's read-only methods from a single actor.
/// No concurrent mutation occurs in our usage.
extension AVPlayerItemVideoOutput: @unchecked @retroactive Sendable {}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
//
// UIView+Extensions.swift
// RenderViewProtocol+Extensions.swift
// GreedyKit
//
// Created by Igor Belov on 05.09.2022.
//

import UIKit

extension BackedRenderView {
extension RenderViewProtocol {
func configure(in superview: UIView) {
translatesAutoresizingMaskIntoConstraints = false

Expand Down
17 changes: 17 additions & 0 deletions Sources/GreedyKit/Helpers/DisplayLinkProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// DisplayLinkProtocol.swift
// GreedyKit
//
// Created by Igor Belov on 04.07.2025.
//

import UIKit

protocol DisplayLinkProtocol: AnyObject {
var isPaused: Bool { get set }

func add(to runLoop: RunLoop, forMode: RunLoop.Mode)
func invalidate()
}

extension CADisplayLink: DisplayLinkProtocol {}
17 changes: 17 additions & 0 deletions Sources/GreedyKit/Helpers/RenderViewProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// RenderViewProtocol.swift
// GreedyKit
//
// Created by Igor Belov on 04.07.2025.
//

import AVFoundation
import UIKit

protocol RenderViewProtocol: UIView {
var preventsCapture: Bool { get set }
var contentGravity: AVLayerVideoGravity { get set }

func enqueueBuffer(_ buffer: CMSampleBuffer) async
func clearLayer() async
}
13 changes: 13 additions & 0 deletions Sources/GreedyKit/Helpers/VideoRendererProtocol.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// VideoRendererProtocol.swift
// GreedyKit
//
// Created by Igor Belov on 04.07.2025.
//

import AVFoundation

protocol VideoRendererProtocol: Sendable {
func attach(to item: AVPlayerItem) async
func frame(at time: CMTime) async -> CMSampleBuffer?
}
87 changes: 59 additions & 28 deletions Sources/GreedyKit/UIKit/GreedyPlayerView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,51 +48,75 @@ public final class GreedyPlayerView: UIView {

// MARK: - Properties

private lazy var renderer = VideoRenderActor()
typealias DisplayLinkFactory = (
_ target: Any,
_ selector: Selector
) -> DisplayLinkProtocol

private let renderView = BackedRenderView()
private var playerItemObserver: AnyCancellable?
private var displayLink: DisplayLinkProtocol?
private let renderer: VideoRendererProtocol
private let renderView: RenderViewProtocol

private lazy var displayLink = CADisplayLink(
target: self,
selector: #selector(displayLinkDidRefresh(link:))
)
private let displayLinkFactory: DisplayLinkFactory
private var playerItemObserver: AnyCancellable?

// MARK: - Lifecycle

public override init(frame: CGRect) {
init(
frame: CGRect = .zero,
renderer: VideoRendererProtocol,
renderView: RenderViewProtocol,
displayLinkFactory: @escaping DisplayLinkFactory
) {
self.renderer = renderer
self.renderView = renderView
self.displayLinkFactory = displayLinkFactory
super.init(frame: frame)

renderView.configure(in: self)
}

public override convenience init(frame: CGRect) {
self.init(
frame: frame,
renderer: VideoRenderer(),
renderView: BackedRenderView(),
displayLinkFactory: { target, selector in
CADisplayLink(target: target, selector: selector)
}
)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

public override func willMove(toSuperview newSuperview: UIView?) {
if newSuperview == nil {
dismantle()
}
}
public override func didMoveToWindow() {
super.didMoveToWindow()

public override func didMoveToSuperview() {
super.didMoveToSuperview()
initializeDisplayLink()
if window == nil {
pauseRendering()
} else {
resumeRendering()
}
}

// MARK: - Private Methods

private func dismantle() {
displayLink.invalidate()
playerItemObserver?.cancel()
private func resumeRendering() {
if displayLink == nil {
displayLink = displayLinkFactory(self, #selector(displayLinkDidRefresh))
displayLink?.add(to: .current, forMode: .common)
}
displayLink?.isPaused = (player?.currentItem == nil)
}

private func initializeDisplayLink() {
displayLink.add(to: .current, forMode: .common)
displayLink.isPaused = true
private func pauseRendering() {
displayLink?.invalidate()
displayLink = nil
}

@objc private func displayLinkDidRefresh(link: CADisplayLink) {
@objc private func displayLinkDidRefresh() {
guard let player else { return }
let itemTime = player.currentTime()

Expand All @@ -104,17 +128,24 @@ public final class GreedyPlayerView: UIView {
}
}

private func attachAndResumeIfNeeded(_ item: AVPlayerItem) async {
if window != nil {
displayLink?.isPaused = false
}
await renderer.attach(to: item)
}

private func addPlayerItemObserver() {
guard let player else { return }
guard let player else {
displayLink?.isPaused = true
return
}

playerItemObserver = player.publisher(for: \.currentItem)
.compactMap { $0 }
.sink { [weak self] item in
guard let self else { return }
Task {
await self.renderer.attach(to: item)
self.displayLink.isPaused = false
}
Task { await attachAndResumeIfNeeded(item) }
}
}
}
52 changes: 0 additions & 52 deletions Tests/GreedyKitTests/GreedyPlayerViewDeinitTests.swift

This file was deleted.

Loading
Loading