Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 6d82e97

Browse files
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-
privatefunc 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)

AltStyle によって変換されたページ (->オリジナル) /