Last active
February 14, 2024 08:48
-
-
Save goodones-mac/dc4f608794a5113a8b2a46b84f1d55d3 to your computer and use it in GitHub Desktop.
This lets you play a HEVC-with-alpha file on repeat and put it in as a SwiftUI view as a much smaller alternative to GIF files. This code is released into the public domain with no warranties, use as you like. Due to it being a SKView, not a UIView you can hypothetically use this within MacOS & iOS without many changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import AVFoundation | |
| import SpriteKit | |
| import SwiftUI | |
| @MainActor | |
| class TransparentBackgroundVideoPlayerUIView: SKView { | |
| let backgroundNode: SKSpriteNode | |
| let videoPlayer: AVPlayer | |
| let videoNode: SKVideoNode | |
| let url: URL | |
| let videoResolution: CGSize? | |
| let keepsAspectRatio: Bool | |
| var repeatLimit: VideoPlayerAlphaView.RepeatCount | |
| private var notificationObserver: NSObjectProtocol? | |
| private var repeatCount = 0 | |
| @MainActor | |
| init(url: URL, keepsAspectRatio: Bool, repeatLimit: VideoPlayerAlphaView.RepeatCount) { | |
| self.url = url | |
| self.keepsAspectRatio = keepsAspectRatio | |
| self.repeatLimit = repeatLimit | |
| videoResolution = Self.resolutionForLocalVideo(url: url) | |
| let scene = SKScene(size: CGSize.zero) | |
| scene.backgroundColor = .clear | |
| backgroundNode = SKSpriteNode(color: .clear, size: CGSize.zero) | |
| scene.addChild(backgroundNode) | |
| videoPlayer = AVPlayer(url: url) | |
| videoNode = SKVideoNode(avPlayer: videoPlayer) | |
| videoNode.name = "videoNode" | |
| scene.addChild(videoNode) | |
| super.init(frame: CGRect.zero) | |
| backgroundColor = .clear | |
| allowsTransparency = true | |
| if videoResolution == nil { | |
| // logger.warn("cannot load video to extract size", extra: ["url": url]) | |
| } | |
| notificationObserver = NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, | |
| object: videoPlayer.currentItem, queue: nil) | |
| { [weak self] _ in | |
| self?.repeatCheck() | |
| } | |
| presentScene(scene) | |
| videoPlayer.play() | |
| setNeedsLayout() | |
| } | |
| deinit { | |
| if let notificationObserver { | |
| NotificationCenter.default.removeObserver(notificationObserver, name: .AVPlayerItemDidPlayToEndTime, object: videoPlayer.currentItem) | |
| } | |
| } | |
| func resetVideo() { | |
| videoPlayer.seek(to: .zero) | |
| videoPlayer.play() | |
| repeatCount += 1 | |
| } | |
| func repeatCheck() { | |
| switch repeatLimit { | |
| case let .constant(limit): | |
| // people 1 index this when they think about this, but the counter is zero indexed, | |
| // so we have to adjust for the zero indexing reality vs. the external API | |
| if repeatCount < limit - 1 { | |
| resetVideo() | |
| } | |
| case .infinite: | |
| resetVideo() | |
| case .never: | |
| break | |
| } | |
| } | |
| @available(*, unavailable) | |
| required init?(coder _: NSCoder) { | |
| fatalError("init(coder:) has not been implemented") | |
| } | |
| override func layoutSubviews() { | |
| super.layoutSubviews() | |
| let size = frame.size | |
| let centerPoint = CGPoint(x: size.width / 2, y: size.height / 2) | |
| scene?.size = size | |
| backgroundNode.size = size | |
| backgroundNode.position = centerPoint | |
| if keepsAspectRatio, let videoResolution { | |
| videoNode.size = videoResolution.aspectFit(into: size) | |
| } else { | |
| videoNode.size = size | |
| } | |
| videoNode.position = centerPoint | |
| } | |
| private static func resolutionForLocalVideo(url: URL) -> CGSize? { | |
| guard let track = AVURLAsset(url: url).tracks(withMediaType: AVMediaType.video).first else { return nil } | |
| let size = track.naturalSize.applying(track.preferredTransform) | |
| return CGSize(width: abs(size.width), height: abs(size.height)) | |
| } | |
| } | |
| struct VideoPlayerAlphaView: UIViewRepresentable { | |
| enum RepeatCount: Equatable { | |
| case infinite | |
| case constant(Int) // How many times it should play, so `.constant(7)` will play 7 times | |
| case never // Will play only once, equivalent to `.constant(1)` | |
| } | |
| let url: URL | |
| let repeatCount: RepeatCount | |
| let keepsAspectRatio: Bool | |
| init(url: URL, repeatCount: RepeatCount = .never, keepsAspectRatio: Bool = true) { | |
| self.url = url | |
| self.repeatCount = repeatCount | |
| self.keepsAspectRatio = keepsAspectRatio | |
| } | |
| func makeUIView(context _: Context) -> TransparentBackgroundVideoPlayerUIView { | |
| TransparentBackgroundVideoPlayerUIView(url: url, keepsAspectRatio: keepsAspectRatio, repeatLimit: repeatCount) | |
| } | |
| func updateUIView(_: TransparentBackgroundVideoPlayerUIView, context _: Context) {} | |
| } | |
| #Preview { | |
| VideoPlayerAlphaView( | |
| url: Bundle.main.url( | |
| forResource: "how_to_use_widget-hevc", | |
| withExtension: "mov" | |
| )! | |
| ) | |
| .frame(width: 300, height: 300) | |
| .background(.green) | |
| } |
Hello, there seems to be an error with your code. in the layoutSubviews().
videoNode.size = videoResolution.aspectFit(into: size)
generates an error Value of type 'CGSize' has no member 'aspectFit'
Is there a missing helper function here?
This CGSize Extension https://stackoverflow.com/a/66923367/12596719 seems to do the trick 👌
Ah yes, good catch. Sorry about that. This is our extension version of that:
import Foundation
extension CGSize {
var area: CGFloat {
width * height
}
func aspectFit(into size: CGSize) -> CGSize {
if width == 0.0 || height == 0.0 {
return self
}
let widthRatio = size.width / width
let heightRatio = size.height / height
let aspectFitRatio = min(widthRatio, heightRatio)
return CGSize(width: width * aspectFitRatio, height: height * aspectFitRatio)
}
func aspectFill(into size: CGSize) -> CGSize {
if width == 0.0 || height == 0.0 {
return self
}
let widthRatio = size.width / width
let heightRatio = size.height / height
let aspectFillRatio = max(widthRatio, heightRatio)
return CGSize(width: width * aspectFillRatio, height: height * aspectFillRatio)
}
}Thank you!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Here is a simpler non-transparent version so you can hide video controls compared to the SwiftUI version: