├── .github ├── meta │ ├── animation.gif │ └── repo-banner.png ├── workflows │ ├── swift.yml │ └── release.yml └── README.md ├── Example ├── Kitsunebi │ ├── logo.png │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Info.plist │ ├── AppDelegate.swift │ ├── ViewController.swift │ ├── Models.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard │ └── ResourceViewController.swift └── Example.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ ├── xcshareddata │ └── xcschemes │ │ └── Kitsunebi-Example.xcscheme │ └── project.pbxproj ├── .swiftpm └── xcode │ ├── package.xcworkspace │ └── contents.xcworkspacedata │ └── xcshareddata │ └── xcschemes │ └── Kitsunebi.xcscheme ├── Tests └── KitsunebiTests │ └── KitsunebiTests.swift ├── Kitsunebi.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Sources └── Kitsunebi │ ├── CVMetalTexture+.swift │ ├── Bundle+.swift │ ├── CVPixelBuffer+.swift │ ├── FPSDebugger.swift │ ├── Frame.swift │ ├── UnsupportedLayer.swift │ ├── Error.swift │ ├── WeakProxy.swift │ ├── CAMetalLayerInterface.swift │ ├── FPSKeeper.swift │ ├── CVMetalTextureCache+.swift │ ├── ApplicationHandler.swift │ ├── Asset.swift │ ├── default.metal │ ├── MTLDevice+.swift │ ├── VideoEngine.swift │ └── AnimationView.swift ├── .gitignore ├── Package.swift └── LICENSE /.github/meta/animation.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noppefoxwolf/Kitsunebi/HEAD/.github/meta/animation.gif -------------------------------------------------------------------------------- /Example/Kitsunebi/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noppefoxwolf/Kitsunebi/HEAD/Example/Kitsunebi/logo.png -------------------------------------------------------------------------------- /.github/meta/repo-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noppefoxwolf/Kitsunebi/HEAD/.github/meta/repo-banner.png -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/KitsunebiTests/KitsunebiTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Kitsunebi 3 | 4 | final class KitsunebiTests: XCTestCase { 5 | func testExample() { 6 | 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Kitsunebi.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Sources/Kitsunebi/CVMetalTexture+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CVMetalTexture+.swift 3 | // Kitsunebi 4 | // 5 | // Created by Tomoya Hirano on 2020/04/02. 6 | // 7 | 8 | import CoreVideo.CVMetalTexture 9 | 10 | extension CVMetalTexture { 11 | var texture: MTLTexture? { CVMetalTextureGetTexture(self) } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/Kitsunebi/Bundle+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Bundle+.swift 3 | // Kitsunebi 4 | // 5 | // Created by Tomoya Hirano on 2020/04/02. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Bundle { 11 | var defaultMetalLibraryURL: URL { 12 | url(forResource: "default", withExtension: "metallib")! 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Kitsunebi.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | gift.mp4 23 | alpha.mp4 24 | 25 | .build -------------------------------------------------------------------------------- /Sources/Kitsunebi/CVPixelBuffer+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CVPixelBuffer+.swift 3 | // Kitsunebi 4 | // 5 | // Created by Tomoya Hirano on 2020/04/02. 6 | // 7 | 8 | import CoreVideo 9 | 10 | extension CVPixelBuffer { 11 | var width: Int { CVPixelBufferGetWidth(self) } 12 | var height: Int { CVPixelBufferGetHeight(self) } 13 | 14 | var size: CGSize { CGSize(width: width, height: height) } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/Kitsunebi/FPSDebugger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FPSDebugger.swift 3 | // Kitsunebi 4 | // 5 | // Created by Tomoya Hirano on 2018/04/13. 6 | // 7 | 8 | import UIKit 9 | 10 | class FPSDebugger { 11 | static let shared = FPSDebugger() 12 | 13 | private var prev: TimeInterval = 0.0 14 | 15 | internal func update(_ link: CADisplayLink) { 16 | debugPrint("\(Int(1.0 / (link.timestamp - prev)))fps") 17 | prev = link.timestamp 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | runs-on: macos-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Run tests 17 | run: | 18 | xcodebuild build -sdk iphoneos -scheme 'Kitsunebi' 19 | xcodebuild test -destination 'name=iPhone 11' -scheme 'Kitsunebi' 20 | -------------------------------------------------------------------------------- /Sources/Kitsunebi/Frame.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Frame.swift 3 | // Kitsunebi 4 | // 5 | // Created by Tomoya Hirano on 2021/02/09. 6 | // 7 | 8 | import CoreGraphics 9 | import CoreVideo 10 | 11 | enum Frame { 12 | case yCbCrWithA(yCbCr: CVImageBuffer, a: CVImageBuffer) 13 | case yCbCrA(yCbCrA: CVImageBuffer) 14 | 15 | var size: CGSize { 16 | switch self { 17 | case let .yCbCrWithA(yCbCr, _): 18 | return yCbCr.size 19 | case let .yCbCrA(yCbCrA): 20 | return yCbCrA.size 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Kitsunebi/UnsupportedLayer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UnsupportedLayer.swift 3 | // Kitsunebi 4 | // 5 | // Created by Tomoya Hirano on 2020/05/26. 6 | // 7 | 8 | import QuartzCore 9 | 10 | class UnsupportedLayer: CALayer, CAMetalLayerInterface { 11 | var pixelFormat: MTLPixelFormat = .a8Unorm 12 | var framebufferOnly: Bool = false 13 | var presentsWithTransaction: Bool = false 14 | var drawableSize: CGSize = .zero 15 | func nextDrawable() -> CAMetalDrawable? { 16 | return nil 17 | } 18 | 19 | required init?(coder: NSCoder) { 20 | super.init(coder: coder) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/Kitsunebi/Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error.swift 3 | // Kitsunebi 4 | // 5 | // Created by Tomoya Hirano on 2018/04/13. 6 | // 7 | 8 | import CoreVideo 9 | import Foundation 10 | 11 | public enum Error: Swift.Error { 12 | case unknown(String) 13 | case cannotAddOutput 14 | } 15 | 16 | internal enum AssetError: Swift.Error { 17 | case readerWasStopped 18 | case readerNotReturnedImage 19 | } 20 | 21 | internal enum RenderError: Swift.Error { 22 | case applicationBackground 23 | case failedToFetchNextDrawable 24 | } 25 | 26 | enum CVMetalError: Swift.Error { 27 | case cvReturn(CVReturn) 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Kitsunebi/WeakProxy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WeakProxy.swift 3 | // AlphaMaskVideoPlayerView 4 | // 5 | // Created by Tomoya Hirano on 2017/12/12. 6 | // 7 | 8 | import Foundation 9 | 10 | final internal class WeakProxy: NSObject { 11 | weak var target: NSObjectProtocol? 12 | 13 | init(target: NSObjectProtocol) { 14 | self.target = target 15 | super.init() 16 | } 17 | 18 | override func responds(to aSelector: Selector!) -> Bool { 19 | return (target?.responds(to: aSelector) ?? false) || super.responds(to: aSelector) 20 | } 21 | 22 | override func forwardingTarget(for aSelector: Selector!) -> Any? { 23 | return target 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Kitsunebi", 8 | platforms: [.iOS(.v13)], 9 | products: [ 10 | .library( 11 | name: "Kitsunebi", 12 | targets: ["Kitsunebi"]), 13 | ], 14 | targets: [ 15 | .target( 16 | name: "Kitsunebi", 17 | resources: [ 18 | .process("default.metal") 19 | ] 20 | ), 21 | .testTarget( 22 | name: "KitsunebiTests", 23 | dependencies: ["Kitsunebi"] 24 | ), 25 | ] 26 | ) 27 | -------------------------------------------------------------------------------- /Sources/Kitsunebi/CAMetalLayerInterface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CAMetalLayerInterface.swift 3 | // Kitsunebi 4 | // 5 | // Created by Tomoya Hirano on 2020/04/02. 6 | // 7 | 8 | import Metal 9 | import QuartzCore.CAMetalLayer 10 | 11 | public protocol CAMetalLayerInterface: AnyObject { 12 | var pixelFormat: MTLPixelFormat { get set } 13 | var framebufferOnly: Bool { get set } 14 | var presentsWithTransaction: Bool { get set } 15 | var drawableSize: CGSize { get set } 16 | func nextDrawable() -> CAMetalDrawable? 17 | } 18 | 19 | #if targetEnvironment(simulator) 20 | @available(iOS 13, *) 21 | extension CAMetalLayer: CAMetalLayerInterface {} 22 | #else 23 | @available(iOS 12, *) 24 | extension CAMetalLayer: CAMetalLayerInterface {} 25 | #endif 26 | -------------------------------------------------------------------------------- /Sources/Kitsunebi/FPSKeeper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FPSKeeper.swift 3 | // Kitsunebi 4 | // 5 | // Created by Tomoya Hirano on 2018/04/13. 6 | // 7 | 8 | import UIKit 9 | 10 | final class FPSKeeper { 11 | private var beforeTimeStamp: CFTimeInterval? = nil 12 | private let timeInterval: CFTimeInterval 13 | init(fps: Int) { 14 | self.timeInterval = floor(1.0 / CFTimeInterval(fps) * 1000.0) / 1000.0 15 | } 16 | 17 | func clear() { 18 | beforeTimeStamp = nil 19 | } 20 | 21 | func checkPast1Frame(_ link: CADisplayLink) -> Bool { 22 | if let beforeTimeStamp = beforeTimeStamp { 23 | guard timeInterval <= link.timestamp - beforeTimeStamp else { 24 | return false 25 | } 26 | } 27 | beforeTimeStamp = link.timestamp 28 | return true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Kitsunebi/CVMetalTextureCache+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CVMetalTextureCache+.swift 3 | // Kitsunebi 4 | // 5 | // Created by Tomoya Hirano on 2020/04/02. 6 | // 7 | 8 | import CoreMedia 9 | import CoreVideo 10 | 11 | extension CVMetalTextureCache { 12 | func flush(options: CVOptionFlags = 0) { 13 | CVMetalTextureCacheFlush(self, options) 14 | } 15 | 16 | func makeTextureFromImage(_ buffer: CVImageBuffer, pixelFormat: MTLPixelFormat, planeIndex: Int) 17 | throws -> CVMetalTexture 18 | { 19 | let width = CVPixelBufferGetWidthOfPlane(buffer, planeIndex) 20 | let height = CVPixelBufferGetHeightOfPlane(buffer, planeIndex) 21 | var imageTexture: CVMetalTexture? 22 | let result = CVMetalTextureCacheCreateTextureFromImage( 23 | kCFAllocatorDefault, 24 | self, 25 | buffer, 26 | nil, 27 | pixelFormat, 28 | width, 29 | height, 30 | planeIndex, 31 | &imageTexture 32 | ) 33 | if let imageTexture = imageTexture { 34 | return imageTexture 35 | } else { 36 | throw CVMetalError.cvReturn(result) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Example/Kitsunebi/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "size" : "1024x1024", 46 | "scale" : "1x" 47 | } 48 | ], 49 | "info" : { 50 | "version" : 1, 51 | "author" : "xcode" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Tomoya Hirano 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Example/Kitsunebi/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | Kitsunebi Demo 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSCameraUsageDescription 26 | 27 | UIFileSharingEnabled 28 | 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIMainStoryboardFile 32 | Main 33 | UIRequiredDeviceCapabilities 34 | 35 | armv7 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Sources/Kitsunebi/ApplicationHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApplicationHandler.swift 3 | // Kitsunebi 4 | // 5 | // Created by Tomoya Hirano on 2018/04/13. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol ApplicationHandlerDelegate: AnyObject { 11 | func willResignActive(_ notification: Notification) 12 | func didBecomeActive(_ notification: Notification) 13 | } 14 | 15 | final class ApplicationHandler { 16 | private(set) var isActive: Bool = true 17 | internal weak var delegate: ApplicationHandlerDelegate? = nil 18 | 19 | init() { 20 | let center = NotificationCenter.default 21 | center.addObserver( 22 | self, selector: #selector(willResignActive), name: UIApplication.willResignActiveNotification, 23 | object: nil) 24 | center.addObserver( 25 | self, selector: #selector(didBecomeActive), name: UIApplication.didBecomeActiveNotification, 26 | object: nil) 27 | } 28 | 29 | deinit { 30 | let center = NotificationCenter.default 31 | center.removeObserver(self) 32 | } 33 | 34 | @objc private func willResignActive(_ notification: Notification) { 35 | isActive = false 36 | delegate?.willResignActive(notification) 37 | } 38 | 39 | @objc private func didBecomeActive(_ notification: Notification) { 40 | isActive = true 41 | delegate?.didBecomeActive(notification) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Sources/Kitsunebi/Asset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Asset.swift 3 | // Kitsunebi 4 | // 5 | // Created by Tomoya Hirano on 2018/04/13. 6 | // 7 | 8 | import AVFoundation 9 | 10 | final class Asset { 11 | private let outputSettings: [String: Any] = [ 12 | kCVPixelBufferMetalCompatibilityKey as String: true 13 | ] 14 | let asset: AVURLAsset 15 | private var reader: AVAssetReader? = nil 16 | private var output: AVAssetReaderTrackOutput? = nil 17 | var status: AVAssetReader.Status? { reader?.status } 18 | 19 | init(url: URL) { 20 | asset = AVURLAsset(url: url) 21 | } 22 | 23 | func reset() throws { 24 | let reader = try AVAssetReader(asset: asset) 25 | let output = AVAssetReaderTrackOutput( 26 | track: asset.tracks(withMediaType: AVMediaType.video)[0], outputSettings: outputSettings) 27 | if reader.canAdd(output) { 28 | reader.add(output) 29 | } else { 30 | throw Error.cannotAddOutput 31 | } 32 | output.alwaysCopiesSampleData = false 33 | reader.startReading() 34 | 35 | self.reader = reader 36 | self.output = output 37 | } 38 | 39 | func cancelReading() { 40 | reader?.cancelReading() 41 | } 42 | 43 | func copyNextImageBuffer() throws -> CVImageBuffer { 44 | if let error = reader?.error { 45 | throw error 46 | } 47 | if status != .reading { 48 | throw AssetError.readerWasStopped 49 | } 50 | if let sampleBuffer = output?.copyNextSampleBuffer(), 51 | let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) 52 | { 53 | return imageBuffer 54 | } else { 55 | throw AssetError.readerNotReturnedImage 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | name: Upload Release Asset 8 | 9 | jobs: 10 | build: 11 | name: Upload Release Asset 12 | runs-on: macos-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | - name: Install Tools 17 | run: | 18 | git clone https://github.com/unsignedapps/swift-create-xcframework.git 19 | cd swift-create-xcframework 20 | make install 21 | - name: Create and Save XCFramework 22 | uses: unsignedapps/swift-create-xcframework@v1.4.0 23 | - name: Create Release 24 | id: create_release 25 | uses: actions/create-release@v1 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | with: 29 | tag_name: ${{ github.ref }} 30 | release_name: Release ${{ github.ref }} 31 | draft: false 32 | prerelease: false 33 | - name: Upload Release Asset 34 | id: upload-release-asset 35 | uses: actions/upload-release-asset@v1 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | with: 39 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 40 | asset_path: ./Kitsunebi.zip 41 | asset_name: Kitsunebi.framework.zip 42 | asset_content_type: application/zip -------------------------------------------------------------------------------- /Sources/Kitsunebi/default.metal: -------------------------------------------------------------------------------- 1 | // 2 | // Kitsunebi.metal 3 | // Kitsunebi_Metal 4 | // 5 | // Created by Tomoya Hirano on 2018/07/19. 6 | // 7 | 8 | #include 9 | using namespace metal; 10 | 11 | struct ColorInOut { 12 | float4 position [[ position ]]; 13 | float2 texCoords; 14 | }; 15 | 16 | vertex ColorInOut vertexShader(uint vid [[ vertex_id ]]) { 17 | const ColorInOut vertices[4] = { 18 | { float4(-1.0f, -1.0f, 0.0f, 1.0f), float2(0.0f, 1.0f) }, 19 | { float4(1.0f, -1.0f, 0.0f, 1.0f), float2(1.0f, 1.0f) }, 20 | { float4(-1.0f, 1.0f, 0.0f, 1.0f), float2(0.0f, 0.0f) }, 21 | { float4(1.0f, 1.0f, 0.0f, 1.0f), float2(1.0f, 0.0f) }, 22 | }; 23 | return vertices[vid]; 24 | } 25 | 26 | fragment float4 fragmentShader(ColorInOut in [[ stage_in ]], 27 | texture2d baseYTexture [[ texture(0) ]], 28 | texture2d alphaYTexture [[ texture(1) ]], 29 | texture2d baseCbCrTexture [[ texture(2) ]]) { 30 | constexpr sampler colorSampler; 31 | const float4x4 ycbcrToRGBTransform = float4x4( 32 | float4(+1.0000f, +1.0000f, +1.0000f, +0.0000f), 33 | float4(+0.0000f, -0.3441f, +1.7720f, +0.0000f), 34 | float4(+1.4020f, -0.7141f, +0.0000f, +0.0000f), 35 | float4(-0.7010f, +0.5291f, -0.8860f, +1.0000f) 36 | ); 37 | 38 | float4 baseColor = ycbcrToRGBTransform * float4(baseYTexture.sample(colorSampler, in.texCoords).r, 39 | baseCbCrTexture.sample(colorSampler, in.texCoords).rg, 40 | 1.0); 41 | 42 | float4 alphaColor = alphaYTexture.sample(colorSampler, in.texCoords).r; 43 | 44 | return float4(baseColor.r, baseColor.g, baseColor.b, alphaColor.r); 45 | } 46 | 47 | 48 | -------------------------------------------------------------------------------- /Sources/Kitsunebi/MTLDevice+.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MTLDevice+.swift 3 | // Kitsunebi 4 | // 5 | // Created by Tomoya Hirano on 2020/04/02. 6 | // 7 | 8 | import CoreVideo 9 | import Metal 10 | 11 | extension MTLDevice { 12 | internal func makeTextureCache() throws -> CVMetalTextureCache { 13 | var textureCache: CVMetalTextureCache? 14 | let result = CVMetalTextureCacheCreate( 15 | kCFAllocatorDefault, 16 | nil, 17 | self, 18 | nil, 19 | &textureCache 20 | ) 21 | if let textureCache = textureCache { 22 | return textureCache 23 | } else { 24 | throw CVMetalError.cvReturn(result) 25 | } 26 | } 27 | 28 | internal func makeTexureCoordBuffer() -> MTLBuffer { 29 | let texCoordinateData: [Float] = [ 30 | 0, 1, 31 | 1, 1, 32 | 0, 0, 33 | 1, 0, 34 | ] 35 | let texCoordinateDataSize = MemoryLayout.size * texCoordinateData.count 36 | return makeBuffer(bytes: texCoordinateData, length: texCoordinateDataSize)! 37 | } 38 | 39 | internal func makeVertexBuffer() -> MTLBuffer { 40 | let vertexData: [Float] = [ 41 | -1.0, -1.0, 0, 1, 42 | 1.0, -1.0, 0, 1, 43 | -1.0, 1.0, 0, 1, 44 | 1.0, 1.0, 0, 1, 45 | ] 46 | let size = vertexData.count * MemoryLayout.size 47 | return makeBuffer(bytes: vertexData, length: size)! 48 | } 49 | 50 | internal func makeRenderPipelineState( 51 | metalLib: MTLLibrary, 52 | pixelFormat: MTLPixelFormat = .bgra8Unorm, 53 | vertexFunctionName: String = "vertexShader", 54 | fragmentFunctionName: String = "fragmentShader" 55 | ) throws -> MTLRenderPipelineState { 56 | let pipelineDesc = MTLRenderPipelineDescriptor() 57 | pipelineDesc.vertexFunction = metalLib.makeFunction(name: vertexFunctionName) 58 | pipelineDesc.fragmentFunction = metalLib.makeFunction(name: fragmentFunctionName) 59 | pipelineDesc.colorAttachments[0].pixelFormat = pixelFormat 60 | 61 | return try makeRenderPipelineState(descriptor: pipelineDesc) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Example/Kitsunebi/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Kitsunebi 4 | // 5 | // Created by Tomoya Hirano on 04/13/2018. 6 | // Copyright (c) 2018 Tomoya Hirano. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 17 | return true 18 | } 19 | 20 | func applicationWillResignActive(_ application: UIApplication) { 21 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 22 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 23 | } 24 | 25 | func applicationDidEnterBackground(_ application: UIApplication) { 26 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 27 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 28 | } 29 | 30 | func applicationWillEnterForeground(_ application: UIApplication) { 31 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 32 | } 33 | 34 | func applicationDidBecomeActive(_ application: UIApplication) { 35 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 36 | } 37 | 38 | func applicationWillTerminate(_ application: UIApplication) { 39 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 40 | } 41 | 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /.github/README.md: -------------------------------------------------------------------------------- 1 | ![](https://github.com/noppefoxwolf/Kitsunebi/blob/main/.github/meta/repo-banner.png) 2 | 3 | Overlay alpha channel video animation player view using Metal. 4 | 5 | ![](https://github.com/noppefoxwolf/Kitsunebi/blob/main/.github/meta/animation.gif) 6 | 7 | ## Example 8 | 9 | To run the example project, clone the repo, and run pod install from the Example directory first. 10 | 11 | ## Usage 12 | 13 | At the top of your file, make sure to `import Kitsunebi` 14 | 15 | ```swift 16 | import Kitsunebi 17 | ``` 18 | 19 | Then, instantiate PlayerView in your view controller: 20 | 21 | ```swift 22 | private lazy var playerView: PlayerView = PlayerView(frame: view.bounds)! 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | view.addSubview(playerView) 27 | } 28 | ``` 29 | 30 | You can play transparency video any framerate. baseVideo is colornize video, alphaVideo is alpha channel monotone video. please see example video files.: 31 | 32 | ```swift 33 | let baseVideoURL = Bundle.main.url(forResource: "base", withExtension: "mp4")! 34 | let alphaVideoURL = Bundle.main.url(forResource: "alpha", withExtension: "mp4")! 35 | playerView.play(base: baseVideoURL, alpha: alphaVideoURL, fps: 30) 36 | ``` 37 | 38 | ## Installation 39 | 40 | Kitsunebi is available SwiftPM and Carthage. To install it, simply add the following line to your SwiftPM: 41 | 42 | ```swift 43 | dependencies: [ 44 | .package(url: "https://github.com/noppefoxwolf/Kitsunebi", .upToNextMajor(from: "v1.0.0")) 45 | ] 46 | ``` 47 | 48 | ``` 49 | github "noppefoxwolf/Kitsunebi" "v1.0.0" 50 | ``` 51 | 52 | ## Author 53 | 54 | Tomoya Hirano, noppelabs@gmail.com 55 | 56 | ## License 57 | 58 | Kitsunebi is available under the MIT license. See the LICENSE file for more info. 59 | 60 | ## Sample video file 61 | 62 | http://basic.ivory.ne.jp 63 | 64 | ## Backers 65 | 66 | 67 | 68 | ## Split video using ffmpeg 69 | 70 | ``` 71 | ffmpeg -i base.mp4 -i alpha.mp4 -filter_complex "nullsrc=size=1500x1334 [base];[0:v] setpts=PTS-STARTPTS, scale=750x1334 [left];[1:v] setpts=PTS-STARTPTS, scale=750x1334 [right];[base][left] overlay=shortest=1 [tmp1];[tmp1][right] overlay=shortest=1:x=750" output.mp4 72 | ``` 73 | -------------------------------------------------------------------------------- /Example/Kitsunebi/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Kitsunebi 4 | // 5 | // Created by Tomoya Hirano on 04/13/2018. 6 | // Copyright (c) 2018 Tomoya Hirano. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AVFoundation 11 | import Kitsunebi 12 | 13 | final class PreviewViewController: UIViewController { 14 | private var currentResource: Resource? = nil 15 | private lazy var camera: AVCaptureDevice? = .default(for: .video) 16 | private lazy var cameraInput: AVCaptureDeviceInput? = try? .init(device: camera!) 17 | private lazy var cameraSession: AVCaptureSession = .init() 18 | private lazy var cameraLayer: AVCaptureVideoPreviewLayer = .init(session: cameraSession) 19 | @IBOutlet private weak var backgroundContentView: UIImageView! 20 | @IBOutlet private weak var playerView: Kitsunebi.PlayerView! 21 | 22 | override func viewDidLoad() { 23 | super.viewDidLoad() 24 | if TARGET_OS_SIMULATOR == 0 { 25 | cameraSession.addInput(cameraInput!) 26 | cameraLayer.frame = view.bounds 27 | cameraLayer.videoGravity = .resizeAspectFill 28 | backgroundContentView.image = nil 29 | backgroundContentView.layer.insertSublayer(cameraLayer, at: 0) 30 | cameraSession.startRunning() 31 | } 32 | playerView.delegate = self 33 | } 34 | 35 | private func play() throws { 36 | guard let resource = currentResource else { return } 37 | switch resource { 38 | case let .twin(resource): 39 | try playerView.play(base: resource.baseVideoURL, alpha: resource.alphaVideoURL, fps: resource.fps) 40 | case let .hevc(resource): 41 | try playerView.play(hevcWithAlpha: resource.hevcWithAlphaVideoURL, fps: resource.fps) 42 | } 43 | } 44 | 45 | @IBAction func tappedResourceButton(_ sender: Any) { 46 | let vc = ResourceViewController.make(selected: currentResource) 47 | vc.delegate = self 48 | let nc = UINavigationController(rootViewController: vc) 49 | present(nc, animated: true, completion: nil) 50 | } 51 | } 52 | 53 | extension PreviewViewController: PlayerViewDelegate { 54 | func playerView(_ playerView: PlayerView, didUpdateFrame index: Int) { 55 | print("INDEX : ", index) 56 | } 57 | 58 | func didFinished(_ playerView: PlayerView) { 59 | do { 60 | try play() 61 | } catch { 62 | debugPrint(error) 63 | } 64 | } 65 | } 66 | 67 | extension PreviewViewController: ResourceViewControllerDelegate { 68 | func resource(_ viewController: ResourceViewController, 69 | didSelected resource: Resource) { 70 | currentResource = resource 71 | viewController.dismiss(animated: true, completion: { [weak self] in 72 | do { 73 | try self?.play() 74 | } catch { 75 | debugPrint(error) 76 | } 77 | }) 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/Kitsunebi.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /Example/Kitsunebi/Models.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Models.swift 3 | // Kitsunebi_Example 4 | // 5 | // Created by Tomoya Hirano on 2018/09/06. 6 | // Copyright © 2018年 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AVFoundation 11 | 12 | final class ResourceStore { 13 | private(set) var resources: [Resource] = [] 14 | 15 | func fetch() throws { 16 | let documentPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] 17 | let url = URL(fileURLWithPath: documentPath) 18 | let directories = try FileManager.default.directoriesOfDirectory(at: url) 19 | resources = directories.compactMap(Resource.init) 20 | } 21 | } 22 | 23 | enum Resource { 24 | case twin(TwinResource) 25 | case hevc(HevcResource) 26 | 27 | var name: String { 28 | switch self { 29 | case let .hevc(resource): 30 | return resource.name 31 | case let .twin(resource): 32 | return resource.name 33 | } 34 | } 35 | 36 | init?(url: URL) { 37 | let hevcResource = HevcResource(dirURL: url) 38 | if hevcResource.hevcWithAlphaVideoSize != nil { 39 | self = .hevc(hevcResource) 40 | return 41 | } 42 | 43 | let twinResource = TwinResource(dirURL: url) 44 | if twinResource.baseVideoSize != nil { 45 | self = .twin(twinResource) 46 | return 47 | } 48 | 49 | return nil 50 | } 51 | } 52 | 53 | struct TwinResource { 54 | let dirURL: URL 55 | let fps: Int = 30 56 | var name: String { dirURL.lastPathComponent } 57 | var baseVideoURL: URL { dirURL.appendingPathComponent("/base.mp4") } 58 | var alphaVideoURL: URL { dirURL.appendingPathComponent("/alpha.mp4") } 59 | 60 | var baseVideoSize: CGSize? { 61 | guard let track = AVAsset(url: baseVideoURL).tracks(withMediaType: .video).first else { return nil } 62 | let size = track.naturalSize.applying(track.preferredTransform) 63 | return CGSize(width: abs(size.width), height: abs(size.height)) 64 | } 65 | 66 | var alphaVideoSize: CGSize? { 67 | guard let track = AVAsset(url: alphaVideoURL).tracks(withMediaType: .video).first else { return nil } 68 | let size = track.naturalSize.applying(track.preferredTransform) 69 | return CGSize(width: abs(size.width), height: abs(size.height)) 70 | } 71 | } 72 | 73 | struct HevcResource { 74 | let dirURL: URL 75 | let fps: Int = 30 76 | var name: String { dirURL.lastPathComponent } 77 | var hevcWithAlphaVideoURL: URL { dirURL.appendingPathComponent("/hevc.mov") } 78 | 79 | var hevcWithAlphaVideoSize: CGSize? { 80 | guard let track = AVAsset(url: hevcWithAlphaVideoURL).tracks(withMediaType: .video).first else { return nil } 81 | let size = track.naturalSize.applying(track.preferredTransform) 82 | return CGSize(width: abs(size.width), height: abs(size.height)) 83 | } 84 | } 85 | 86 | extension FileManager { 87 | func directoriesOfDirectory(at url: URL) throws -> [URL] { 88 | let contents = try contentsOfDirectory(at: url, 89 | includingPropertiesForKeys: [.isDirectoryKey], 90 | options: FileManager.DirectoryEnumerationOptions(rawValue: 0)) 91 | return try contents.filter({ (try $0.resourceValues(forKeys: [.isDirectoryKey]).isDirectory) ?? false }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Example/Kitsunebi/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 25 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /Example/Kitsunebi/ResourceViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResourceViewController.swift 3 | // Kitsunebi_Example 4 | // 5 | // Created by Tomoya Hirano on 2018/09/06. 6 | // Copyright © 2018年 CocoaPods. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol ResourceViewControllerDelegate: AnyObject { 12 | func resource(_ viewController: ResourceViewController, didSelected resource: Resource) 13 | } 14 | 15 | final class ResourceViewController: UIViewController { 16 | private var selectedResource: Resource? = nil 17 | private let resourceStore: ResourceStore = .init() 18 | private lazy var noticeLabel: UILabel = .init(frame: view.bounds) 19 | private lazy var tableView: UITableView = .init(frame: view.bounds) 20 | weak var delegate: ResourceViewControllerDelegate? = nil 21 | private let emptyResourceMessage: String = """ 22 | Transfer arbitrary directories using Finder. 23 | There are base.mp4 and alpha.mp4 or hevc.mov. 24 | ex: Burning/base.mp4 and Burning/alpha.mp4 or Burning/hevc.mov 25 | """ 26 | 27 | static func make(selected resource: Resource?) -> ResourceViewController { 28 | let vc = ResourceViewController() 29 | vc.selectedResource = resource 30 | return vc 31 | } 32 | 33 | override func viewDidLoad() { 34 | super.viewDidLoad() 35 | 36 | tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell") 37 | 38 | noticeLabel.font = .systemFont(ofSize: 12) 39 | noticeLabel.numberOfLines = 0 40 | noticeLabel.backgroundColor = .systemBackground 41 | noticeLabel.textAlignment = .center 42 | view.addSubview(noticeLabel) 43 | 44 | tableView.delegate = self 45 | tableView.dataSource = self 46 | tableView.backgroundColor = .clear 47 | tableView.tableFooterView = UIView() 48 | view.addSubview(tableView) 49 | 50 | let left = UIBarButtonItem(barButtonSystemItem: .cancel, 51 | target: self, 52 | action: #selector(tappedCloseButton)) 53 | navigationItem.leftBarButtonItem = left 54 | 55 | let right = UIBarButtonItem(barButtonSystemItem: .refresh, 56 | target: self, 57 | action: #selector(tappedReloadButton)) 58 | navigationItem.rightBarButtonItem = right 59 | 60 | reloadData() 61 | } 62 | 63 | @objc private func tappedCloseButton(_ sender: UIBarButtonItem) { 64 | dismiss(animated: true, completion: nil) 65 | } 66 | 67 | @objc private func tappedReloadButton(_ sender: UIBarButtonItem) { 68 | reloadData() 69 | } 70 | 71 | private func reloadData() { 72 | try! resourceStore.fetch() 73 | noticeLabel.text = resourceStore.resources.isEmpty ? emptyResourceMessage : "" 74 | tableView.reloadData() 75 | } 76 | } 77 | 78 | extension ResourceViewController: UITableViewDelegate, UITableViewDataSource { 79 | func tableView(_ tableView: UITableView, 80 | numberOfRowsInSection section: Int) -> Int { 81 | return resourceStore.resources.count 82 | } 83 | 84 | func tableView(_ tableView: UITableView, 85 | cellForRowAt indexPath: IndexPath) -> UITableViewCell { 86 | let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "cell") 87 | let resource = resourceStore.resources[indexPath.row] 88 | cell.textLabel?.text = resource.name 89 | 90 | switch resource { 91 | case let .hevc(resource): 92 | let hevcWithAlphaSize = resource.hevcWithAlphaVideoSize 93 | cell.detailTextLabel?.text = "HEVC with Alpha: w\(hevcWithAlphaSize!.width) x h\(hevcWithAlphaSize!.height)" 94 | case let .twin(resource): 95 | let mSize = resource.baseVideoSize 96 | let aSize = resource.alphaVideoSize 97 | let baseText = mSize != nil ? "Base w\(mSize!.width) x h\(mSize!.height)" : "base.mp4 not found" 98 | let alphaText = aSize != nil ? "Alpha: w\(aSize!.width) x h\(aSize!.height)" : "alpha.mp4 not found" 99 | cell.detailTextLabel?.text = "\(baseText) / \(alphaText)" 100 | } 101 | 102 | if let selected = selectedResource, selected.name == resource.name { 103 | cell.accessoryType = .checkmark 104 | } else { 105 | cell.accessoryType = .none 106 | } 107 | return cell 108 | } 109 | 110 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 111 | let resource = resourceStore.resources[indexPath.row] 112 | delegate?.resource(self, didSelected: resource) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/xcshareddata/xcschemes/Kitsunebi-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 51 | 52 | 53 | 54 | 56 | 62 | 63 | 64 | 66 | 72 | 73 | 74 | 75 | 76 | 86 | 88 | 94 | 95 | 96 | 97 | 103 | 105 | 111 | 112 | 113 | 114 | 116 | 117 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /Sources/Kitsunebi/VideoEngine.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VideoEngine.swift 3 | // Kitsunebi 4 | // 5 | // Created by Tomoya Hirano on 2018/04/13. 6 | // 7 | 8 | import AVFoundation 9 | import CoreImage 10 | 11 | internal protocol VideoEngineUpdateDelegate: AnyObject { 12 | func didOutputFrame(_ frame: Frame) 13 | func didReceiveError(_ error: Swift.Error?) 14 | func didCompleted() 15 | } 16 | 17 | internal protocol VideoEngineDelegate: AnyObject { 18 | func didUpdateFrame(_ index: Int, engine: VideoEngine) 19 | func engineDidFinishPlaying(_ engine: VideoEngine) 20 | } 21 | 22 | enum VideoEngineAsset { 23 | case yCbCrWithA(yCbCr: Asset, a: Asset) 24 | case yCbCrA(yCbCrA: Asset) 25 | } 26 | 27 | internal class VideoEngine: NSObject { 28 | private let asset: VideoEngineAsset 29 | private let fpsKeeper: FPSKeeper 30 | private lazy var displayLink: CADisplayLink = .init( 31 | target: WeakProxy(target: self), selector: #selector(VideoEngine.update)) 32 | internal weak var delegate: VideoEngineDelegate? = nil 33 | internal weak var updateDelegate: VideoEngineUpdateDelegate? = nil 34 | private var isRunningTheread = true 35 | private lazy var renderThread: Thread = .init( 36 | target: WeakProxy(target: self), selector: #selector(VideoEngine.threadLoop), object: nil) 37 | private lazy var currentFrameIndex: Int = 0 38 | 39 | public init(base baseVideoURL: URL, alpha alphaVideoURL: URL, fps: Int) { 40 | let baseAsset = Asset(url: baseVideoURL) 41 | let alphaAsset = Asset(url: alphaVideoURL) 42 | asset = .yCbCrWithA(yCbCr: baseAsset, a: alphaAsset) 43 | fpsKeeper = FPSKeeper(fps: fps) 44 | super.init() 45 | renderThread.start() 46 | } 47 | 48 | public init(hevcWithAlpha hevcWithAlphaVideoURL: URL, fps: Int) { 49 | let hevcWithAlphaAsset = Asset(url: hevcWithAlphaVideoURL) 50 | asset = .yCbCrA(yCbCrA: hevcWithAlphaAsset) 51 | fpsKeeper = FPSKeeper(fps: fps) 52 | super.init() 53 | renderThread.start() 54 | } 55 | 56 | @objc private func threadLoop() { 57 | displayLink.add(to: .current, forMode: .common) 58 | displayLink.isPaused = true 59 | if #available(iOS 10.0, *) { 60 | displayLink.preferredFramesPerSecond = 0 61 | } else { 62 | displayLink.frameInterval = 1 63 | } 64 | while isRunningTheread { 65 | RunLoop.current.run(until: Date(timeIntervalSinceNow: 1 / 60)) 66 | } 67 | } 68 | 69 | func purge() { 70 | isRunningTheread = false 71 | } 72 | 73 | deinit { 74 | displayLink.remove(from: .current, forMode: .common) 75 | displayLink.invalidate() 76 | } 77 | 78 | private func reset() throws { 79 | switch asset { 80 | case let .yCbCrA(yCbCrA): 81 | try yCbCrA.reset() 82 | case let .yCbCrWithA(yCbCr, a): 83 | try yCbCr.reset() 84 | try a.reset() 85 | } 86 | } 87 | 88 | private func cancelReading() { 89 | switch asset { 90 | case let .yCbCrA(yCbCrA): 91 | yCbCrA.cancelReading() 92 | case let .yCbCrWithA(yCbCr, a): 93 | yCbCr.cancelReading() 94 | a.cancelReading() 95 | } 96 | } 97 | 98 | public func play() throws { 99 | try reset() 100 | displayLink.isPaused = false 101 | } 102 | 103 | public func pause() { 104 | guard !isCompleted else { return } 105 | displayLink.isPaused = true 106 | } 107 | 108 | public func resume() { 109 | guard !isCompleted else { return } 110 | displayLink.isPaused = false 111 | } 112 | 113 | private func finish() { 114 | displayLink.isPaused = true 115 | fpsKeeper.clear() 116 | updateDelegate?.didCompleted() 117 | delegate?.engineDidFinishPlaying(self) 118 | purge() 119 | } 120 | 121 | @objc private func update(_ link: CADisplayLink) { 122 | guard fpsKeeper.checkPast1Frame(link) else { return } 123 | 124 | #if DEBUG 125 | FPSDebugger.shared.update(link) 126 | #endif 127 | 128 | autoreleasepool(invoking: { [weak self] in 129 | self?.updateFrame() 130 | }) 131 | } 132 | 133 | private var isCompleted: Bool { 134 | switch asset { 135 | case let .yCbCrA(yCbCrA): 136 | return yCbCrA.status == .completed 137 | case let .yCbCrWithA(yCbCr, a): 138 | return yCbCr.status == .completed || a.status == .completed 139 | } 140 | } 141 | 142 | private func updateFrame() { 143 | guard !displayLink.isPaused else { return } 144 | if isCompleted { 145 | finish() 146 | return 147 | } 148 | do { 149 | let frame = try copyNextFrame() 150 | updateDelegate?.didOutputFrame(frame) 151 | 152 | currentFrameIndex += 1 153 | delegate?.didUpdateFrame(currentFrameIndex, engine: self) 154 | } catch (let error) { 155 | updateDelegate?.didReceiveError(error) 156 | finish() 157 | } 158 | } 159 | 160 | private func copyNextFrame() throws -> Frame { 161 | switch asset { 162 | case let .yCbCrA(yCbCrA): 163 | let yCbCrABuffer = try yCbCrA.copyNextImageBuffer() 164 | return .yCbCrA(yCbCrA: yCbCrABuffer) 165 | case let .yCbCrWithA(yCbCr, a): 166 | let yCbCrBuffer = try yCbCr.copyNextImageBuffer() 167 | let aBuffer = try a.copyNextImageBuffer() 168 | return .yCbCrWithA(yCbCr: yCbCrBuffer, a: aBuffer) 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /Example/Kitsunebi/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /Sources/Kitsunebi/AnimationView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PlayerView.swift 3 | // Kitsunebi 4 | // 5 | // Created by Tomoya Hirano on 2018/04/13. 6 | // 7 | 8 | import MetalKit 9 | import UIKit 10 | 11 | public protocol PlayerViewDelegate: AnyObject { 12 | func playerView(_ playerView: PlayerView, didUpdateFrame index: Int) 13 | func didFinished(_ playerView: PlayerView) 14 | } 15 | 16 | open class PlayerView: UIView { 17 | typealias LayerClass = CAMetalLayerInterface & CALayer 18 | override open class var layerClass: Swift.AnyClass { 19 | return CAMetalLayer.self 20 | } 21 | // self.layer main thread使う必要あるので、事前に持つことでmain thread以外のthreadでも使えるように 22 | private lazy var gpuLayer: LayerClass = { fatalError("gpuLayer must be init") }() 23 | private let renderQueue: DispatchQueue = DispatchQueue(label: "dev.noppe.kitsunebi.PlayerView.renderQueue", qos: .userInitiated) 24 | private let commandQueue: MTLCommandQueue 25 | private let textureCache: CVMetalTextureCache 26 | private let pipelineState: MTLRenderPipelineState 27 | private var applicationHandler = ApplicationHandler() 28 | 29 | public weak var delegate: PlayerViewDelegate? = nil 30 | internal var engineInstance: VideoEngine? = nil 31 | 32 | public func play(base baseVideoURL: URL, alpha alphaVideoURL: URL, fps: Int) throws { 33 | engineInstance?.purge() 34 | engineInstance = VideoEngine(base: baseVideoURL, alpha: alphaVideoURL, fps: fps) 35 | engineInstance?.updateDelegate = self 36 | engineInstance?.delegate = self 37 | try engineInstance?.play() 38 | } 39 | 40 | public func play(hevcWithAlpha hevcWithAlphaVideoURL: URL, fps: Int) throws { 41 | engineInstance?.purge() 42 | engineInstance = VideoEngine(hevcWithAlpha: hevcWithAlphaVideoURL, fps: fps) 43 | engineInstance?.updateDelegate = self 44 | engineInstance?.delegate = self 45 | try engineInstance?.play() 46 | } 47 | 48 | public init?(frame: CGRect, device: MTLDevice? = MTLCreateSystemDefaultDevice()) { 49 | guard let device = device else { return nil } 50 | guard let commandQueue = device.makeCommandQueue() else { return nil } 51 | guard let textureCache = try? device.makeTextureCache() else { return nil } 52 | guard let metalLib = try? device.makeLibrary(URL: Bundle.module.defaultMetalLibraryURL) else { 53 | return nil 54 | } 55 | guard let pipelineState = try? device.makeRenderPipelineState(metalLib: metalLib) else { 56 | return nil 57 | } 58 | self.commandQueue = commandQueue 59 | self.textureCache = textureCache 60 | self.pipelineState = pipelineState 61 | super.init(frame: frame) 62 | applicationHandler.delegate = self 63 | backgroundColor = .clear 64 | 65 | gpuLayer = self.layer as! LayerClass 66 | gpuLayer.isOpaque = false 67 | gpuLayer.drawsAsynchronously = true 68 | gpuLayer.contentsGravity = .resizeAspectFill 69 | gpuLayer.pixelFormat = .bgra8Unorm 70 | gpuLayer.framebufferOnly = false 71 | gpuLayer.presentsWithTransaction = false 72 | } 73 | 74 | required public init?(coder aDecoder: NSCoder) { 75 | guard let device = MTLCreateSystemDefaultDevice() else { return nil } 76 | guard let commandQueue = device.makeCommandQueue() else { return nil } 77 | guard let textureCache = try? device.makeTextureCache() else { return nil } 78 | guard let metalLib = try? device.makeLibrary(URL: Bundle.module.defaultMetalLibraryURL) else { 79 | return nil 80 | } 81 | guard let pipelineState = try? device.makeRenderPipelineState(metalLib: metalLib) else { 82 | return nil 83 | } 84 | self.commandQueue = commandQueue 85 | self.textureCache = textureCache 86 | self.pipelineState = pipelineState 87 | super.init(coder: aDecoder) 88 | applicationHandler.delegate = self 89 | backgroundColor = .clear 90 | 91 | gpuLayer = self.layer as! LayerClass 92 | gpuLayer.isOpaque = false 93 | gpuLayer.drawsAsynchronously = true 94 | gpuLayer.contentsGravity = .resizeAspectFill 95 | gpuLayer.pixelFormat = .bgra8Unorm 96 | gpuLayer.framebufferOnly = false 97 | gpuLayer.presentsWithTransaction = false 98 | } 99 | 100 | deinit { 101 | NotificationCenter.default.removeObserver(self) 102 | } 103 | 104 | private func renderImage(with frame: Frame, to nextDrawable: CAMetalDrawable) throws { 105 | dispatchPrecondition(condition: .onQueue(renderQueue)) 106 | 107 | let (baseYTexture, baseCbCrTexture, alphaYTexture) = try makeTexturesFrom(frame) 108 | 109 | let renderDesc = MTLRenderPassDescriptor() 110 | renderDesc.colorAttachments[0].texture = nextDrawable.texture 111 | renderDesc.colorAttachments[0].loadAction = .clear 112 | 113 | if let commandBuffer = commandQueue.makeCommandBuffer(), 114 | let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderDesc) 115 | { 116 | renderEncoder.setRenderPipelineState(pipelineState) 117 | renderEncoder.setFragmentTexture(baseYTexture, index: 0) 118 | renderEncoder.setFragmentTexture(alphaYTexture, index: 1) 119 | renderEncoder.setFragmentTexture(baseCbCrTexture, index: 2) 120 | renderEncoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4) 121 | renderEncoder.endEncoding() 122 | 123 | commandBuffer.present(nextDrawable) 124 | commandBuffer.commit() 125 | commandBuffer.waitUntilCompleted() 126 | } 127 | 128 | textureCache.flush() 129 | } 130 | 131 | private func clear() { 132 | renderQueue.async { [weak self] in 133 | guard let nextDrawable = self?.gpuLayer.nextDrawable() else { return } 134 | self?.clear(nextDrawable: nextDrawable) 135 | } 136 | } 137 | 138 | private func clear(nextDrawable: CAMetalDrawable) { 139 | // render queueで実行する必要がある 140 | dispatchPrecondition(condition: .onQueue(renderQueue)) 141 | 142 | let renderPassDescriptor = MTLRenderPassDescriptor() 143 | renderPassDescriptor.colorAttachments[0].texture = nextDrawable.texture 144 | renderPassDescriptor.colorAttachments[0].loadAction = .clear 145 | renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor( 146 | red: 0, green: 0, blue: 0, alpha: 0) 147 | 148 | let commandBuffer = commandQueue.makeCommandBuffer() 149 | let renderEncoder = commandBuffer?.makeRenderCommandEncoder(descriptor: renderPassDescriptor) 150 | renderEncoder?.endEncoding() 151 | commandBuffer?.present(nextDrawable) 152 | commandBuffer?.commit() 153 | textureCache.flush() 154 | } 155 | 156 | private func makeTexturesFrom(_ frame: Frame) throws -> ( 157 | y: MTLTexture?, cbcr: MTLTexture?, a: MTLTexture? 158 | ) { 159 | let baseYTexture: MTLTexture? 160 | let baseCbCrTexture: MTLTexture? 161 | let alphaYTexture: MTLTexture? 162 | 163 | switch frame { 164 | case let .yCbCrWithA(yCbCr, a): 165 | let basePixelBuffer = yCbCr 166 | let alphaPixelBuffer = a 167 | let alphaPlaneIndex = 0 168 | baseYTexture = try textureCache.makeTextureFromImage( 169 | basePixelBuffer, pixelFormat: .r8Unorm, planeIndex: 0 170 | ).texture 171 | baseCbCrTexture = try textureCache.makeTextureFromImage( 172 | basePixelBuffer, pixelFormat: .rg8Unorm, planeIndex: 1 173 | ).texture 174 | alphaYTexture = try textureCache.makeTextureFromImage( 175 | alphaPixelBuffer, pixelFormat: .r8Unorm, planeIndex: alphaPlaneIndex 176 | ).texture 177 | 178 | case let .yCbCrA(yCbCrA): 179 | let basePixelBuffer = yCbCrA 180 | let alphaPixelBuffer = yCbCrA 181 | let alphaPlaneIndex = 2 182 | baseYTexture = try textureCache.makeTextureFromImage( 183 | basePixelBuffer, pixelFormat: .r8Unorm, planeIndex: 0 184 | ).texture 185 | baseCbCrTexture = try textureCache.makeTextureFromImage( 186 | basePixelBuffer, pixelFormat: .rg8Unorm, planeIndex: 1 187 | ).texture 188 | alphaYTexture = try textureCache.makeTextureFromImage( 189 | alphaPixelBuffer, pixelFormat: .r8Unorm, planeIndex: alphaPlaneIndex 190 | ).texture 191 | } 192 | 193 | return (baseYTexture, baseCbCrTexture, alphaYTexture) 194 | } 195 | } 196 | 197 | extension PlayerView: VideoEngineUpdateDelegate { 198 | internal func didOutputFrame(_ frame: Frame) { 199 | renderQueue.async { [weak self] in 200 | guard let self = self, self.applicationHandler.isActive else { return } 201 | guard let nextDrawable = self.gpuLayer.nextDrawable() else { return } 202 | self.gpuLayer.drawableSize = frame.size 203 | do { 204 | try self.renderImage(with: frame, to: nextDrawable) 205 | } catch { 206 | self.clear(nextDrawable: nextDrawable) 207 | } 208 | } 209 | } 210 | 211 | internal func didReceiveError(_ error: Swift.Error?) { 212 | guard applicationHandler.isActive else { return } 213 | clear() 214 | } 215 | 216 | internal func didCompleted() { 217 | guard applicationHandler.isActive else { return } 218 | clear() 219 | } 220 | } 221 | 222 | extension PlayerView: VideoEngineDelegate { 223 | internal func didUpdateFrame(_ index: Int, engine: VideoEngine) { 224 | delegate?.playerView(self, didUpdateFrame: index) 225 | } 226 | 227 | internal func engineDidFinishPlaying(_ engine: VideoEngine) { 228 | delegate?.didFinished(self) 229 | } 230 | } 231 | 232 | extension PlayerView: ApplicationHandlerDelegate { 233 | func didBecomeActive(_ notification: Notification) { 234 | engineInstance?.resume() 235 | } 236 | 237 | func willResignActive(_ notification: Notification) { 238 | engineInstance?.pause() 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /Example/Example.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD51AFB9204008FA782 /* AppDelegate.swift */; }; 11 | 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 607FACD71AFB9204008FA782 /* ViewController.swift */; }; 12 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 607FACD91AFB9204008FA782 /* Main.storyboard */; }; 13 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDC1AFB9204008FA782 /* Images.xcassets */; }; 14 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */; }; 15 | D72C379721410B7800F6D22F /* ResourceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72C379621410B7800F6D22F /* ResourceViewController.swift */; }; 16 | D72C379A21410B9E00F6D22F /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72C379921410B9E00F6D22F /* Models.swift */; }; 17 | D7791985208744A1000C3E18 /* logo.png in Resources */ = {isa = PBXBuildFile; fileRef = D7791984208744A0000C3E18 /* logo.png */; }; 18 | D79E80962680C522007F8F4A /* Kitsunebi in Frameworks */ = {isa = PBXBuildFile; productRef = D79E80952680C522007F8F4A /* Kitsunebi */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXFileReference section */ 22 | 607FACD01AFB9204008FA782 /* iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 23 | 607FACD41AFB9204008FA782 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 24 | 607FACD51AFB9204008FA782 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 25 | 607FACD71AFB9204008FA782 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 26 | 607FACDA1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 27 | 607FACDC1AFB9204008FA782 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 28 | 607FACDF1AFB9204008FA782 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 29 | D72C379621410B7800F6D22F /* ResourceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResourceViewController.swift; sourceTree = ""; }; 30 | D72C379921410B9E00F6D22F /* Models.swift */ = {isa = PBXFileReference; indentWidth = 2; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; tabWidth = 2; }; 31 | D7791984208744A0000C3E18 /* logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = logo.png; sourceTree = ""; }; 32 | D79E80942680C4C6007F8F4A /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Package.swift; path = ../Package.swift; sourceTree = ""; }; 33 | /* End PBXFileReference section */ 34 | 35 | /* Begin PBXFrameworksBuildPhase section */ 36 | 607FACCD1AFB9204008FA782 /* Frameworks */ = { 37 | isa = PBXFrameworksBuildPhase; 38 | buildActionMask = 2147483647; 39 | files = ( 40 | D79E80962680C522007F8F4A /* Kitsunebi in Frameworks */, 41 | ); 42 | runOnlyForDeploymentPostprocessing = 0; 43 | }; 44 | /* End PBXFrameworksBuildPhase section */ 45 | 46 | /* Begin PBXGroup section */ 47 | 607FACC71AFB9204008FA782 = { 48 | isa = PBXGroup; 49 | children = ( 50 | 607FACD21AFB9204008FA782 /* Example */, 51 | 607FACD11AFB9204008FA782 /* Products */, 52 | B05CCBFE864607607BF5EAC1 /* Frameworks */, 53 | ); 54 | sourceTree = ""; 55 | }; 56 | 607FACD11AFB9204008FA782 /* Products */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | 607FACD01AFB9204008FA782 /* iOS.app */, 60 | ); 61 | name = Products; 62 | sourceTree = ""; 63 | }; 64 | 607FACD21AFB9204008FA782 /* Example */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | D72C379821410B8E00F6D22F /* Model */, 68 | D72C379521410B6700F6D22F /* Controller */, 69 | D7791984208744A0000C3E18 /* logo.png */, 70 | 607FACD51AFB9204008FA782 /* AppDelegate.swift */, 71 | 607FACD71AFB9204008FA782 /* ViewController.swift */, 72 | 607FACD91AFB9204008FA782 /* Main.storyboard */, 73 | 607FACDC1AFB9204008FA782 /* Images.xcassets */, 74 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */, 75 | 607FACD31AFB9204008FA782 /* Supporting Files */, 76 | ); 77 | name = Example; 78 | path = Kitsunebi; 79 | sourceTree = ""; 80 | }; 81 | 607FACD31AFB9204008FA782 /* Supporting Files */ = { 82 | isa = PBXGroup; 83 | children = ( 84 | 607FACD41AFB9204008FA782 /* Info.plist */, 85 | ); 86 | name = "Supporting Files"; 87 | sourceTree = ""; 88 | }; 89 | B05CCBFE864607607BF5EAC1 /* Frameworks */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | D79E80942680C4C6007F8F4A /* Package.swift */, 93 | ); 94 | name = Frameworks; 95 | sourceTree = ""; 96 | }; 97 | D72C379521410B6700F6D22F /* Controller */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | D72C379621410B7800F6D22F /* ResourceViewController.swift */, 101 | ); 102 | name = Controller; 103 | sourceTree = ""; 104 | }; 105 | D72C379821410B8E00F6D22F /* Model */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | D72C379921410B9E00F6D22F /* Models.swift */, 109 | ); 110 | name = Model; 111 | sourceTree = ""; 112 | }; 113 | /* End PBXGroup section */ 114 | 115 | /* Begin PBXNativeTarget section */ 116 | 607FACCF1AFB9204008FA782 /* iOS */ = { 117 | isa = PBXNativeTarget; 118 | buildConfigurationList = 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "iOS" */; 119 | buildPhases = ( 120 | 607FACCC1AFB9204008FA782 /* Sources */, 121 | 607FACCD1AFB9204008FA782 /* Frameworks */, 122 | 607FACCE1AFB9204008FA782 /* Resources */, 123 | ); 124 | buildRules = ( 125 | ); 126 | dependencies = ( 127 | ); 128 | name = iOS; 129 | packageProductDependencies = ( 130 | D79E80952680C522007F8F4A /* Kitsunebi */, 131 | ); 132 | productName = Kitsunebi; 133 | productReference = 607FACD01AFB9204008FA782 /* iOS.app */; 134 | productType = "com.apple.product-type.application"; 135 | }; 136 | /* End PBXNativeTarget section */ 137 | 138 | /* Begin PBXProject section */ 139 | 607FACC81AFB9204008FA782 /* Project object */ = { 140 | isa = PBXProject; 141 | attributes = { 142 | DefaultBuildSystemTypeForWorkspace = Original; 143 | LastSwiftUpdateCheck = 1220; 144 | LastUpgradeCheck = 1250; 145 | ORGANIZATIONNAME = CocoaPods; 146 | TargetAttributes = { 147 | 607FACCF1AFB9204008FA782 = { 148 | CreatedOnToolsVersion = 6.3.1; 149 | LastSwiftMigration = 0900; 150 | ProvisioningStyle = Automatic; 151 | }; 152 | }; 153 | }; 154 | buildConfigurationList = 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "Example" */; 155 | compatibilityVersion = "Xcode 3.2"; 156 | developmentRegion = en; 157 | hasScannedForEncodings = 0; 158 | knownRegions = ( 159 | en, 160 | Base, 161 | ); 162 | mainGroup = 607FACC71AFB9204008FA782; 163 | productRefGroup = 607FACD11AFB9204008FA782 /* Products */; 164 | projectDirPath = ""; 165 | projectRoot = ""; 166 | targets = ( 167 | 607FACCF1AFB9204008FA782 /* iOS */, 168 | ); 169 | }; 170 | /* End PBXProject section */ 171 | 172 | /* Begin PBXResourcesBuildPhase section */ 173 | 607FACCE1AFB9204008FA782 /* Resources */ = { 174 | isa = PBXResourcesBuildPhase; 175 | buildActionMask = 2147483647; 176 | files = ( 177 | 607FACDB1AFB9204008FA782 /* Main.storyboard in Resources */, 178 | 607FACE01AFB9204008FA782 /* LaunchScreen.xib in Resources */, 179 | D7791985208744A1000C3E18 /* logo.png in Resources */, 180 | 607FACDD1AFB9204008FA782 /* Images.xcassets in Resources */, 181 | ); 182 | runOnlyForDeploymentPostprocessing = 0; 183 | }; 184 | /* End PBXResourcesBuildPhase section */ 185 | 186 | /* Begin PBXSourcesBuildPhase section */ 187 | 607FACCC1AFB9204008FA782 /* Sources */ = { 188 | isa = PBXSourcesBuildPhase; 189 | buildActionMask = 2147483647; 190 | files = ( 191 | 607FACD81AFB9204008FA782 /* ViewController.swift in Sources */, 192 | 607FACD61AFB9204008FA782 /* AppDelegate.swift in Sources */, 193 | D72C379721410B7800F6D22F /* ResourceViewController.swift in Sources */, 194 | D72C379A21410B9E00F6D22F /* Models.swift in Sources */, 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | }; 198 | /* End PBXSourcesBuildPhase section */ 199 | 200 | /* Begin PBXVariantGroup section */ 201 | 607FACD91AFB9204008FA782 /* Main.storyboard */ = { 202 | isa = PBXVariantGroup; 203 | children = ( 204 | 607FACDA1AFB9204008FA782 /* Base */, 205 | ); 206 | name = Main.storyboard; 207 | sourceTree = ""; 208 | }; 209 | 607FACDE1AFB9204008FA782 /* LaunchScreen.xib */ = { 210 | isa = PBXVariantGroup; 211 | children = ( 212 | 607FACDF1AFB9204008FA782 /* Base */, 213 | ); 214 | name = LaunchScreen.xib; 215 | sourceTree = ""; 216 | }; 217 | /* End PBXVariantGroup section */ 218 | 219 | /* Begin XCBuildConfiguration section */ 220 | 607FACED1AFB9204008FA782 /* Debug */ = { 221 | isa = XCBuildConfiguration; 222 | buildSettings = { 223 | ALWAYS_SEARCH_USER_PATHS = NO; 224 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 225 | CLANG_CXX_LIBRARY = "libc++"; 226 | CLANG_ENABLE_MODULES = YES; 227 | CLANG_ENABLE_OBJC_ARC = YES; 228 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 229 | CLANG_WARN_BOOL_CONVERSION = YES; 230 | CLANG_WARN_COMMA = YES; 231 | CLANG_WARN_CONSTANT_CONVERSION = YES; 232 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 233 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 234 | CLANG_WARN_EMPTY_BODY = YES; 235 | CLANG_WARN_ENUM_CONVERSION = YES; 236 | CLANG_WARN_INFINITE_RECURSION = YES; 237 | CLANG_WARN_INT_CONVERSION = YES; 238 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 239 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 240 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 241 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 242 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 243 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 244 | CLANG_WARN_STRICT_PROTOTYPES = YES; 245 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 246 | CLANG_WARN_UNREACHABLE_CODE = YES; 247 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 248 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 249 | COPY_PHASE_STRIP = NO; 250 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 251 | ENABLE_STRICT_OBJC_MSGSEND = YES; 252 | ENABLE_TESTABILITY = YES; 253 | GCC_C_LANGUAGE_STANDARD = gnu99; 254 | GCC_DYNAMIC_NO_PIC = NO; 255 | GCC_NO_COMMON_BLOCKS = YES; 256 | GCC_OPTIMIZATION_LEVEL = 0; 257 | GCC_PREPROCESSOR_DEFINITIONS = ( 258 | "DEBUG=1", 259 | "$(inherited)", 260 | ); 261 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 262 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 263 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 264 | GCC_WARN_UNDECLARED_SELECTOR = YES; 265 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 266 | GCC_WARN_UNUSED_FUNCTION = YES; 267 | GCC_WARN_UNUSED_VARIABLE = YES; 268 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 269 | MTL_ENABLE_DEBUG_INFO = YES; 270 | ONLY_ACTIVE_ARCH = YES; 271 | SDKROOT = iphoneos; 272 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 273 | SWIFT_VERSION = 5.0; 274 | }; 275 | name = Debug; 276 | }; 277 | 607FACEE1AFB9204008FA782 /* Release */ = { 278 | isa = XCBuildConfiguration; 279 | buildSettings = { 280 | ALWAYS_SEARCH_USER_PATHS = NO; 281 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 282 | CLANG_CXX_LIBRARY = "libc++"; 283 | CLANG_ENABLE_MODULES = YES; 284 | CLANG_ENABLE_OBJC_ARC = YES; 285 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 286 | CLANG_WARN_BOOL_CONVERSION = YES; 287 | CLANG_WARN_COMMA = YES; 288 | CLANG_WARN_CONSTANT_CONVERSION = YES; 289 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 290 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 291 | CLANG_WARN_EMPTY_BODY = YES; 292 | CLANG_WARN_ENUM_CONVERSION = YES; 293 | CLANG_WARN_INFINITE_RECURSION = YES; 294 | CLANG_WARN_INT_CONVERSION = YES; 295 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 296 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 297 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 298 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 299 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 300 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 301 | CLANG_WARN_STRICT_PROTOTYPES = YES; 302 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 303 | CLANG_WARN_UNREACHABLE_CODE = YES; 304 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 305 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 306 | COPY_PHASE_STRIP = NO; 307 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 308 | ENABLE_NS_ASSERTIONS = NO; 309 | ENABLE_STRICT_OBJC_MSGSEND = YES; 310 | GCC_C_LANGUAGE_STANDARD = gnu99; 311 | GCC_NO_COMMON_BLOCKS = YES; 312 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 313 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 314 | GCC_WARN_UNDECLARED_SELECTOR = YES; 315 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 316 | GCC_WARN_UNUSED_FUNCTION = YES; 317 | GCC_WARN_UNUSED_VARIABLE = YES; 318 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 319 | MTL_ENABLE_DEBUG_INFO = NO; 320 | SDKROOT = iphoneos; 321 | SWIFT_COMPILATION_MODE = wholemodule; 322 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 323 | SWIFT_VERSION = 5.0; 324 | VALIDATE_PRODUCT = YES; 325 | }; 326 | name = Release; 327 | }; 328 | 607FACF01AFB9204008FA782 /* Debug */ = { 329 | isa = XCBuildConfiguration; 330 | buildSettings = { 331 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 332 | CODE_SIGN_IDENTITY = "iPhone Developer"; 333 | CODE_SIGN_STYLE = Automatic; 334 | DEVELOPMENT_TEAM = 582LEGWZJK; 335 | INFOPLIST_FILE = Kitsunebi/Info.plist; 336 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 337 | LD_RUNPATH_SEARCH_PATHS = ( 338 | "$(inherited)", 339 | "@executable_path/Frameworks", 340 | ); 341 | MODULE_NAME = ExampleApp; 342 | PRODUCT_BUNDLE_IDENTIFIER = "dev.noppe.kitsunebi-example"; 343 | PRODUCT_NAME = "$(TARGET_NAME)"; 344 | PROVISIONING_PROFILE = ""; 345 | PROVISIONING_PROFILE_SPECIFIER = ""; 346 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 347 | }; 348 | name = Debug; 349 | }; 350 | 607FACF11AFB9204008FA782 /* Release */ = { 351 | isa = XCBuildConfiguration; 352 | buildSettings = { 353 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 354 | CODE_SIGN_IDENTITY = "iPhone Developer"; 355 | CODE_SIGN_STYLE = Automatic; 356 | DEVELOPMENT_TEAM = 582LEGWZJK; 357 | INFOPLIST_FILE = Kitsunebi/Info.plist; 358 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 359 | LD_RUNPATH_SEARCH_PATHS = ( 360 | "$(inherited)", 361 | "@executable_path/Frameworks", 362 | ); 363 | MODULE_NAME = ExampleApp; 364 | PRODUCT_BUNDLE_IDENTIFIER = "dev.noppe.kitsunebi-example"; 365 | PRODUCT_NAME = "$(TARGET_NAME)"; 366 | PROVISIONING_PROFILE_SPECIFIER = ""; 367 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 368 | }; 369 | name = Release; 370 | }; 371 | /* End XCBuildConfiguration section */ 372 | 373 | /* Begin XCConfigurationList section */ 374 | 607FACCB1AFB9204008FA782 /* Build configuration list for PBXProject "Example" */ = { 375 | isa = XCConfigurationList; 376 | buildConfigurations = ( 377 | 607FACED1AFB9204008FA782 /* Debug */, 378 | 607FACEE1AFB9204008FA782 /* Release */, 379 | ); 380 | defaultConfigurationIsVisible = 0; 381 | defaultConfigurationName = Release; 382 | }; 383 | 607FACEF1AFB9204008FA782 /* Build configuration list for PBXNativeTarget "iOS" */ = { 384 | isa = XCConfigurationList; 385 | buildConfigurations = ( 386 | 607FACF01AFB9204008FA782 /* Debug */, 387 | 607FACF11AFB9204008FA782 /* Release */, 388 | ); 389 | defaultConfigurationIsVisible = 0; 390 | defaultConfigurationName = Release; 391 | }; 392 | /* End XCConfigurationList section */ 393 | 394 | /* Begin XCSwiftPackageProductDependency section */ 395 | D79E80952680C522007F8F4A /* Kitsunebi */ = { 396 | isa = XCSwiftPackageProductDependency; 397 | productName = Kitsunebi; 398 | }; 399 | /* End XCSwiftPackageProductDependency section */ 400 | }; 401 | rootObject = 607FACC81AFB9204008FA782 /* Project object */; 402 | } 403 | --------------------------------------------------------------------------------