Skip to content

Commit 6d82e97

Browse files
committed
added Picture-in-Picture support
1 parent be93d12 commit 6d82e97

File tree

12 files changed

+158
-9
lines changed

12 files changed

+158
-9
lines changed

Sources/swiftui-loop-videoplayer/enum/PlaybackCommand.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,23 @@ public enum PlaybackCommand: Equatable {
9595
/// Command to select a specific audio track based on language code.
9696
/// - Parameter languageCode: The language code (e.g., "en" for English) of the desired audio track.
9797
case audioTrack(languageCode: String)
98-
98+
99+
#if os(iOS)
100+
case startPiP
101+
102+
case stopPiP
103+
#endif
104+
99105
public static func == (lhs: PlaybackCommand, rhs: PlaybackCommand) -> Bool {
100106
switch (lhs, rhs) {
101107
case (.idle, .idle), (.play, .play), (.pause, .pause), (.begin, .begin), (.end, .end),
102108
(.mute, .mute), (.unmute, .unmute), (.loop, .loop), (.unloop, .unloop),
103109
(.removeAllFilters, .removeAllFilters), (.removeAllVectors, .removeAllVectors):
104110
return true
105-
111+
#if os(iOS)
112+
case (.startPiP, .startPiP), (.stopPiP, .stopPiP):
113+
return true
114+
#endif
106115
case (.seek(let lhsTime, let lhsPlay), .seek(let rhsTime, let rhsPlay)):
107116
return lhsTime == rhsTime && lhsPlay == rhsPlay
108117

Sources/swiftui-loop-videoplayer/enum/PlayerEvent.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ public enum PlayerEvent: Equatable {
6262

6363

6464
case boundsChanged(CGRect)
65+
66+
case startedPiP
67+
68+
case stoppedPiP
6569
}
6670

6771
extension PlayerEvent: CustomStringConvertible {
@@ -85,6 +89,10 @@ extension PlayerEvent: CustomStringConvertible {
8589
return "\(e.description)"
8690
case .boundsChanged(let bounds):
8791
return "Bounds changed \(bounds)"
92+
case .startedPiP:
93+
return "Started PiP"
94+
case .stoppedPiP:
95+
return "Stopped PiP"
8896
}
8997
}
9098
}

Sources/swiftui-loop-videoplayer/enum/Setting.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ public enum Setting: Equatable, SettingsConvertible{
4242
/// Subtitles
4343
case subtitles(String)
4444

45+
/// Support Picture-in-Picture
46+
case pictureInPicture
47+
4548
/// A CMTime value representing the interval at which the player's current time should be published.
4649
/// If set, the player will publish periodic time updates based on this interval.
4750
case timePublishing(CMTime)

Sources/swiftui-loop-videoplayer/enum/VPErrors.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ public enum VPErrors: Error, CustomStringConvertible, Sendable{
2222
/// Error case for when settings are not unique.
2323
case settingsNotUnique
2424

25+
/// Picture-in-Picture (PiP) is not supported
26+
case notSupportedPiP
27+
2528
/// A description of the error, suitable for display.
2629
public var description: String {
2730
switch self {
@@ -30,6 +33,9 @@ public enum VPErrors: Error, CustomStringConvertible, Sendable{
3033
case .sourceNotFound(let name):
3134
return "Source not found: \(name)"
3235

36+
case .notSupportedPiP:
37+
return "Picture-in-Picture (PiP) is not supported on this device."
38+
3339
/// Returns a description indicating that the settings are not unique.
3440
case .settingsNotUnique:
3541
return "Settings are not unique"

Sources/swiftui-loop-videoplayer/protocol/helpers/PlayerDelegateProtocol.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77

88
import Foundation
99
import AVFoundation
10+
#if os(iOS)
11+
import AVKit
12+
#endif
1013

1114
/// Protocol to handle player-related errors.
1215
///
1316
/// Conforming to this protocol allows a class to respond to error events that occur within a media player context.
1417
@available(iOS 14, macOS 11, tvOS 14, *)
1518
@MainActor
16-
public protocol PlayerDelegateProtocol: AnyObject {
19+
public protocol PlayerDelegateProtocol: AnyObject{
1720
/// Called when an error is encountered within the media player.
1821
///
1922
/// This method provides a way for delegate objects to respond to error conditions, allowing them to handle or
@@ -69,4 +72,10 @@ public protocol PlayerDelegateProtocol: AnyObject {
6972
/// - Parameter bounds: The new bounds of the main layer where we keep the video player and all vector layers. This allows a developer to recalculate and update all vector layers that lie in the CompositeLayer.
7073

7174
func boundsDidChange(to bounds: CGRect)
75+
76+
#if os(iOS)
77+
func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController)
78+
79+
func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController)
80+
#endif
7281
}

Sources/swiftui-loop-videoplayer/protocol/player/AbstractPlayer.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import AVFoundation
99
#if canImport(CoreImage)
1010
import CoreImage
11+
import AVKit
1112
#endif
1213

1314
/// Defines an abstract player protocol to be implemented by player objects, ensuring main-thread safety and compatibility with specific OS versions.
@@ -18,6 +19,10 @@ public protocol AbstractPlayer: AnyObject {
1819

1920
// MARK: - Properties
2021

22+
#if os(iOS)
23+
var pipController: AVPictureInPictureController? { get set }
24+
#endif
25+
2126
/// An optional property that stores the current video settings.
2227
///
2328
/// This property holds an instance of `VideoSettings` or nil if no settings have been configured yet.
@@ -445,4 +450,25 @@ extension AbstractPlayer{
445450
}
446451
#endif
447452
}
453+
454+
#if os(iOS)
455+
func startPiP() {
456+
guard let pipController = pipController else { return }
457+
458+
if !pipController.isPictureInPictureActive {
459+
pipController.startPictureInPicture()
460+
461+
}
462+
}
463+
464+
func stopPiP() {
465+
guard let pipController = pipController else { return }
466+
467+
if pipController.isPictureInPictureActive {
468+
// Stop PiP
469+
pipController.stopPictureInPicture()
470+
}
471+
}
472+
473+
#endif
448474
}

Sources/swiftui-loop-videoplayer/protocol/player/ExtPlayerProtocol.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,9 @@ public protocol ExtPlayerProtocol: AbstractPlayer, LayerMakerProtocol{
6363
/// - item: The AVPlayerItem to observe for status changes.
6464
/// - player: The AVQueuePlayer to observe for errors.
6565
func setupObservers(for player: AVQueuePlayer)
66-
66+
67+
/// Handles errors
68+
func onError(_ error : VPErrors)
6769
}
6870

6971
internal extension ExtPlayerProtocol {
@@ -193,7 +195,7 @@ internal extension ExtPlayerProtocol {
193195

194196
/// Handles errors
195197
/// - Parameter error: An instance of `VPErrors` representing the error to be handled.
196-
private func onError(_ error : VPErrors){
198+
func onError(_ error : VPErrors){
197199
delegate?.didReceiveError(error)
198200
}
199201

@@ -351,6 +353,10 @@ internal extension ExtPlayerProtocol {
351353
addVectorLayer(builder: builder, clear: clear)
352354
case .removeAllVectors:
353355
removeAllVectors()
356+
#if os(iOS)
357+
case .startPiP: startPiP()
358+
case .stopPiP: stopPiP()
359+
#endif
354360
default : return
355361
}
356362
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// PictureInPicture .swift
3+
//
4+
//
5+
// Created by Igor Shelopaev on 21.01.25.
6+
//
7+
8+
import Foundation
9+
10+
11+
/// Represents a settings structure that enables looping functionality, conforming to `SettingsConvertible`.
12+
@available(iOS 14.0, macOS 11.0, tvOS 14.0, *)
13+
public struct PictureInPicture : SettingsConvertible{
14+
15+
// MARK: - Life circle
16+
17+
/// Initializes a new instance
18+
public init() {}
19+
20+
/// Fetch settings
21+
@_spi(Private)
22+
public func asSettings() -> [Setting] {
23+
[.pictureInPicture]
24+
}
25+
}

Sources/swiftui-loop-videoplayer/utils/VideoSettings.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ public struct VideoSettings: Equatable{
2626
/// Loop video
2727
public let loop: Bool
2828

29+
/// Loop video
30+
public let pictureInPicture: Bool
31+
2932
/// Mute video
3033
public let mute: Bool
3134

@@ -63,11 +66,12 @@ public struct VideoSettings: Equatable{
6366
/// - notAutoPlay: A Boolean indicating whether the video should not auto-play.
6467
/// - timePublishing: A `CMTime` value representing the interval for time publishing updates, or `nil`.
6568
/// - gravity: The `AVLayerVideoGravity` value defining how the video should be displayed in its layer.
66-
public init(name: String, ext: String, subtitles: String, loop: Bool, mute: Bool, notAutoPlay: Bool, timePublishing: CMTime?, gravity: AVLayerVideoGravity, enableVector : Bool = false) {
69+
public init(name: String, ext: String, subtitles: String, loop: Bool, pictureInPicture: Bool, mute: Bool, notAutoPlay: Bool, timePublishing: CMTime?, gravity: AVLayerVideoGravity, enableVector : Bool = false) {
6770
self.name = name
6871
self.ext = ext
6972
self.subtitles = subtitles
7073
self.loop = loop
74+
self.pictureInPicture = pictureInPicture
7175
self.mute = mute
7276
self.notAutoPlay = notAutoPlay
7377
self.timePublishing = timePublishing
@@ -95,6 +99,8 @@ public struct VideoSettings: Equatable{
9599

96100
loop = settings.contains(.loop)
97101

102+
pictureInPicture = settings.contains(.pictureInPicture)
103+
98104
mute = settings.contains(.mute)
99105

100106
notAutoPlay = settings.contains(.notAutoPlay)
@@ -108,7 +114,7 @@ public extension VideoSettings {
108114

109115
/// Returns a new instance of VideoSettings with loop set to false and notAutoPlay set to true, keeping other settings unchanged.
110116
var settingsWithAutoPlay : VideoSettings {
111-
VideoSettings(name: self.name, ext: self.ext, subtitles: self.subtitles, loop: self.loop, mute: self.mute, notAutoPlay: false, timePublishing: self.timePublishing, gravity: self.gravity, enableVector: self.vector)
117+
VideoSettings(name: self.name, ext: self.ext, subtitles: self.subtitles, loop: self.loop, pictureInPicture: self.pictureInPicture, mute: self.mute, notAutoPlay: false, timePublishing: self.timePublishing, gravity: self.gravity, enableVector: self.vector)
112118
}
113119

114120
func getAssets()-> AVURLAsset?{

Sources/swiftui-loop-videoplayer/view/helpers/PlayerCoordinator.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import SwiftUI
99
import Combine
1010
import AVFoundation
11+
#if os(iOS)
12+
import AVKit
13+
#endif
1114

1215
@MainActor
1316
internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol {
@@ -119,4 +122,22 @@ internal class PlayerCoordinator: NSObject, PlayerDelegateProtocol {
119122
func boundsDidChange(to bounds: CGRect) {
120123
eventPublisher.send(.boundsChanged(bounds))
121124
}
125+
126+
}
127+
128+
#if os(iOS)
129+
extension PlayerCoordinator: AVPictureInPictureControllerDelegate{
130+
131+
nonisolated func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
132+
Task{ @MainActor in
133+
eventPublisher.send(.startedPiP)
134+
}
135+
}
136+
137+
nonisolated func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
138+
Task{ @MainActor in
139+
eventPublisher.send(.stoppedPiP)
140+
}
141+
}
122142
}
143+
#endif

0 commit comments

Comments
 (0)