├── .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 | 
2 |
3 | Overlay alpha channel video animation player view using Metal.
4 |
5 | 
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 |
--------------------------------------------------------------------------------