Skip to content

Commit

Permalink
Add FullscreenState
Browse files Browse the repository at this point in the history
  • Loading branch information
SvenTiigi committed Feb 2, 2025
1 parent 6b10ab5 commit ffe6bde
Show file tree
Hide file tree
Showing 10 changed files with 298 additions and 67 deletions.
21 changes: 18 additions & 3 deletions Sources/API/YouTubePlayer+PlaybackAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,21 @@ public extension YouTubePlayer {
await self.webView.closeAllMediaPresentations()
}

/// The current fullscreen state.
var fullscreenState: FullscreenState? {
self.fullscreenStateSubject.value
}

/// A Publisher that emits the fullscreen state.
var fullscreenStatePublisher: some Publisher<FullscreenState, Never> {
self.fullscreenStateSubject
.compactMap { $0 }
.removeDuplicates()
.receive(on: DispatchQueue.main)
}

/// Requests web fullscreen mode, applicable only if `configuration.fullscreenMode` is set to `.web`.
/// - Important: This function only executes if the configuration's ``YouTubePlayer.FullscreenMode`` of the ``YouTubePlayer.Configuration`` is set to `.web`.
/// - Important: This function only executes if the configuration's ``YouTubePlayer/FullscreenMode`` of the ``YouTubePlayer/Configuration`` is set to `.web`.
/// - Returns: A boolean value indicating whether the fullscreen request was successful:
/// `true` if fullscreen mode was successfully requested.
/// `false` if either the configuration doesn't allow web fullscreen or if the fullscreen API is not available
Expand All @@ -325,16 +338,18 @@ public extension YouTubePlayer {
}

/// The web fullscreen state of the underlying ``WKWebView`` instance.
/// - Important: This property only indicates the fullscreen state when the ``YouTubePlayer.FullscreenMode`` of the ``YouTubePlayer.Configuration`` is set to `.web`.
/// - Important: This property only indicates the fullscreen state when the ``YouTubePlayer/FullscreenMode`` of the ``YouTubePlayer/Configuration`` is set to `.web`.
/// **It does not reflect the fullscreen state when playing a video in fullscreen using the `.system` mode.**
/// - SeeAlso: ``YouTubePlayer/fullscreenState-swift.property``, ``YouTubePlayer/fullscreenStatePublisher``
@available(iOS 16.0, macOS 13.0, visionOS 1.0, *)
var webFullscreenState: WebKit.WKWebView.FullscreenState {
self.webView.fullscreenState
}

/// A Publisher that emits the web fullscreen state of the underlying ``WKWebView`` instance.
/// - Important: The value of this publisher only indicates the fullscreen state when the ``YouTubePlayer.FullscreenMode`` of the ``YouTubePlayer.Configuration`` is set to `.web`.
/// - Important: The value of this publisher only indicates the fullscreen state when the ``YouTubePlayer/FullscreenMode`` of the ``YouTubePlayer/Configuration`` is set to `.web`.
/// **It does not reflect the fullscreen state when playing a video in fullscreen using the `.system` mode.**
/// - SeeAlso: ``YouTubePlayer/fullscreenState-swift.property``, ``YouTubePlayer/fullscreenStatePublisher``
@available(iOS 16.0, macOS 13.0, visionOS 1.0, *)
var webFullScreenStatePublisher: some Publisher<WebKit.WKWebView.FullscreenState, Never> {
self.webView
Expand Down
107 changes: 107 additions & 0 deletions Sources/Models/JavaScript/YouTubePlayer+JavaScriptEvent+Data.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import Foundation

// MARK: - YouTubePlayer+JavaScriptEvent+Data

public extension YouTubePlayer.JavaScriptEvent {

/// A YouTube player JavaScript event data payload.
struct Data: Hashable, Sendable {

// MARK: Properties

/// The value.
public let value: String

// MARK: Initializer

/// Creates a new instance of ``YouTubePlayer.JavaScriptEvent.Data``
/// - Parameter value: The value
public init(
value: String
) {
self.value = value
}

}

}

// MARK: - Convenience Initializer

public extension YouTubePlayer.JavaScriptEvent.Data {

/// Creates a new instance of ``YouTubePlayer.JavaScriptEvent.Data``
/// - Parameter value: The value
init?(
urlQueryItem: URLQueryItem
) {
// Verify value of query item is available and is not empty and not equal to "null"
guard let value = urlQueryItem.value?.trimmingCharacters(in: .whitespacesAndNewlines),
!value.isEmpty,
value.lowercased() != "null" else {
// Otherwise return out of function
return nil
}
// Initialize with value
self.init(value: value)
}

}

// MARK: - Codable

extension YouTubePlayer.JavaScriptEvent.Data: Codable {

/// Creates a new instance of ``YouTubePlayer.JavaScriptEvent.Data``
/// - Parameter decoder: The decoder.
public init(
from decoder: Decoder
) throws {
let container = try decoder.singleValueContainer()
self.init(
value: try container.decode(String.self)
)
}

/// Encode.
/// - Parameter encoder: The encoder.
public func encode(
to encoder: Encoder
) throws {
var container = encoder.singleValueContainer()
try container.encode(self.value)
}

}

extension YouTubePlayer.JavaScriptEvent.Data: CustomStringConvertible {

public var description: String {
self.value
}

}

public extension YouTubePlayer.JavaScriptEvent.Data {

func value<T: LosslessStringConvertible>(
as type: T.Type
) -> T? {
.init(self.value)
}

}

public extension YouTubePlayer.JavaScriptEvent.Data {

func jsonValue<D: Decodable>(
as type: D.Type,
jsonDecoder: JSONDecoder = .init()
) throws -> D {
try jsonDecoder.decode(
D.self,
from: .init(self.value.utf8)
)
}

}
31 changes: 31 additions & 0 deletions Sources/Models/JavaScript/YouTubePlayer+JavaScriptEvent+Name.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Foundation

// MARK: - Name

public extension YouTubePlayer.JavaScriptEvent {

/// The YouTubePlayer JavaScriptEvent Name
enum Name: String, Codable, Hashable, Sendable, CaseIterable {
/// iFrame Api is ready
case onIframeApiReady
/// iFrame Api failed to load
case onIframeApiFailedToLoad
/// Error
case onError
/// Ready
case onReady
/// API Change
case onApiChange
/// Autoplay blocked
case onAutoplayBlocked
/// State change
case onStateChange
/// Playback quality change
case onPlaybackQualityChange
/// Playback rate change
case onPlaybackRateChange
/// Fullscreen change
case onFullscreenChange
}

}
38 changes: 5 additions & 33 deletions Sources/Models/JavaScript/YouTubePlayer+JavaScriptEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ public extension YouTubePlayer {
/// The name.
public var name: Name

/// The optional data.
public var data: String?
/// The data.
public var data: Data?

// MARK: Initializer

/// Creates a new instance of ``YouTubePlayer.JavaScriptEvent``
/// - Parameters:
/// - name: The name.
/// - data: The optional data. Default value `nil`
/// - data: The data. Default value `nil`
public init(
name: Name,
data: String? = nil
data: Data? = nil
) {
self.name = name
self.data = data
Expand All @@ -41,36 +41,8 @@ extension YouTubePlayer.JavaScriptEvent: CustomStringConvertible {
public var description: String {
"""
Name: \(self.name.rawValue)
Data: \(self.data ?? "nil")
Data: \(self.data?.value ?? "nil")
"""
}

}

// MARK: - Name

public extension YouTubePlayer.JavaScriptEvent {

/// The YouTubePlayer JavaScriptEvent Name
enum Name: String, Codable, Hashable, Sendable, CaseIterable {
/// iFrame Api is ready
case onIframeApiReady
/// iFrame Api failed to load
case onIframeApiFailedToLoad
/// Ready
case onReady
/// State change
case onStateChange
/// Playback quality change
case onPlaybackQualityChange
/// Playback rate change
case onPlaybackRateChange
/// Error
case onError
/// API Change
case onApiChange
/// Autoplay blocked
case onAutoplayBlocked
}

}
2 changes: 2 additions & 0 deletions Sources/Models/YouTubePlayer+Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public extension YouTubePlayer {
// MARK: Properties

/// The fullscreen mode.
/// - Important: Setting the fullscreen mode to ``YouTubePlayer/FullscreenMode/web`` does not guarantee
/// the use of HTML5 fullscreen mode, as the decision ultimately depends on the underlying YouTube Player iFrame API.
public let fullscreenMode: FullscreenMode

/// A Boolean value that indicates whether HTML5 videos play inline or use the native full-screen controller.
Expand Down
2 changes: 1 addition & 1 deletion Sources/Models/YouTubePlayer+FullscreenMode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public extension YouTubePlayer {
enum FullscreenMode: String, Codable, Hashable, Sendable, CaseIterable {
/// System fullscreen mode (AVPlayerViewController).
case system
/// Web fullscreen mode (YouTube web player)
/// Web fullscreen mode (HTML5)
case web
}

Expand Down
93 changes: 93 additions & 0 deletions Sources/Models/YouTubePlayer+FullscreenState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import Foundation

// MARK: - YouTubePlayer

public extension YouTubePlayer {

/// A YouTube player fullscreen state.
struct FullscreenState: Hashable, Sendable {

// MARK: Properties

/// A Boolean indicating whether it is in fullscreen or not.
public let isFullscreen: Bool

/// The video identifier.
public let videoID: String?

/// The time.
public let time: Measurement<UnitDuration>?

// MARK: Initializer

/// Creates a new instance of ``YouTubePlayer.FullscreenState``
/// - Parameters:
/// - isFullscreen: A Boolean indicating whether it is in fullscreen or not.
/// - videoID: The video identifier. Default value `nil`
/// - time: The time. Default value `nil`
public init(
isFullscreen: Bool,
videoID: String? = nil,
time: Measurement<UnitDuration>? = nil
) {
self.isFullscreen = isFullscreen
self.videoID = videoID
self.time = time
}

}

}

// MARK: - Codable

extension YouTubePlayer.FullscreenState: Codable {

/// The coding keys.
private enum CodingKeys: String, CodingKey {
case isFullscreen = "fullscreen"
case videoID = "videoId"
case time
}

/// Creates a new instance of ``YouTubePlayer.FullscreenState``
/// - Parameter decoder: The decoder.
public init(
from decoder: Decoder
) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
try self.init(
isFullscreen: container.decode(Bool.self, forKey: .isFullscreen),
videoID: container.decodeIfPresent(String.self, forKey: .videoID),
time: container.decodeIfPresent(Double.self, forKey: .time).flatMap { .init(value: $0, unit: .seconds) }
)
}

/// Encode.
/// - Parameter encoder: The encoder.
public func encode(
to encoder: Encoder
) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.isFullscreen, forKey: .isFullscreen)
try container.encode(self.videoID, forKey: .videoID)
try container.encode(self.time?.converted(to: .seconds).value, forKey: .time)
}

}

// MARK: - ExpressibleByBooleanLiteral

extension YouTubePlayer.FullscreenState: ExpressibleByBooleanLiteral {

/// Creates a new instance of ``YouTubePlayer.FullscreenState``
/// - Parameter isFullscreen: A Boolean indicating whether it is in fullscreen or not.
public init(
booleanLiteral isFullscreen: Bool
) {
self.init(
isFullscreen: isFullscreen
)
}

}
5 changes: 4 additions & 1 deletion Sources/Models/YouTubePlayer+HTMLBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,10 @@ public extension YouTubePlayer.HTMLBuilder {
function sendYouTubePlayerEvent(eventName, event) {
const url = new URL(`\(htmlBuilder.youTubePlayerEventCallbackURLScheme)://${eventName}`);
if (event && event.data !== null) {
url.searchParams.set('\(htmlBuilder.youTubePlayerEventCallbackDataParameterName)', event.data);
url.searchParams.set(
'\(htmlBuilder.youTubePlayerEventCallbackDataParameterName)',
typeof event.data === 'object' ? JSON.stringify(event.data) : event.data
);
}
window.location.href = url.toString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,8 @@ extension YouTubePlayerWebView: WKNavigationDelegate {
resolvingAgainstBaseURL: true
)?
.queryItems?
.first { $0.name == self.player?.configuration.htmlBuilder.youTubePlayerEventCallbackDataParameterName }?
.value
.flatMap { $0 == "null" ? nil : $0 }
.first { $0.name == self.player?.configuration.htmlBuilder.youTubePlayerEventCallbackDataParameterName }
.flatMap(YouTubePlayer.JavaScriptEvent.Data.init)
)
// Log received JavaScript event
self.player?
Expand Down
Loading

0 comments on commit ffe6bde

Please sign in to comment.