├── .DS_Store
├── .gitignore
├── Dependencies
├── .DS_Store
└── CommonCrypto.swift
│ ├── .gitignore
│ ├── .slather.yml
│ ├── .travis.yml
│ ├── CONTRIBUTING.md
│ ├── CommonCryptoSwift.podspec
│ ├── CommonCryptoSwift.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── CommonCryptoSwift-iOS.xcscheme
│ ├── CommonCryptoSwift
│ ├── Info-Mac.plist
│ └── Info-iOS.plist
│ ├── CommonCryptoSwiftTests
│ ├── Info-Mac.plist
│ ├── Info-iOS.plist
│ └── Tests.swift
│ ├── Example
│ └── CommonCryptoSwiftDemo
│ │ ├── CommonCryptoSwiftDemo.xcodeproj
│ │ ├── project.pbxproj
│ │ ├── project.xcworkspace
│ │ │ └── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── CommonCryptoSwiftDemo.xcscheme
│ │ ├── CommonCryptoSwiftDemo.xcworkspace
│ │ └── contents.xcworkspacedata
│ │ ├── CommonCryptoSwiftDemo
│ │ ├── Base.lproj
│ │ │ └── LaunchScreen.storyboard
│ │ ├── Info.plist
│ │ ├── Resources
│ │ │ └── Assets.xcassets
│ │ │ │ └── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ └── Sources
│ │ │ ├── AppDelegate.swift
│ │ │ └── ViewController.swift
│ │ ├── Podfile
│ │ └── Podfile.lock
│ ├── LICENSE.md
│ ├── Playground-Mac.playground
│ ├── Contents.swift
│ └── contents.xcplayground
│ ├── Playground-iOS.playground
│ ├── Contents.swift
│ └── contents.xcplayground
│ ├── README.md
│ ├── Screenshots
│ └── Banner.png
│ └── Sources
│ ├── CCommonCrypto
│ └── module.modulemap
│ ├── Crypto.swift
│ ├── HMAC.swift
│ ├── Hash.swift
│ └── NSData+Extensions.swift
├── LICENSE
├── README.md
├── Sodes
├── .DS_Store
├── Sodes.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── Cast.xcscmblueprint
│ │ │ └── Sodes.xcscmblueprint
│ └── xcshareddata
│ │ └── xcschemes
│ │ ├── SodesAudio.xcscheme
│ │ ├── SodesFoundation.xcscheme
│ │ └── SwiftableFileHandle.xcscheme
├── SodesAudio
│ ├── .DS_Store
│ ├── AVContentInfoRequest.swift
│ ├── ByteRange.swift
│ ├── Data.swift
│ ├── DataRequestLoader.swift
│ ├── Errors.swift
│ ├── Info.plist
│ ├── MiscExtensions.swift
│ ├── NSObject.swift
│ ├── PlaybackController.swift
│ ├── PlaybackSource.swift
│ ├── Requests.swift
│ ├── ResourceLoaderDelegate.swift
│ ├── ResourceLoaderSubrequest.swift
│ ├── SODSwiftableFileHandle.swift
│ ├── ScratchFileInfo.swift
│ ├── ScratchFileInfoSerialization.swift
│ ├── URL.swift
│ ├── URLRequest.swift
│ └── URLResponse.swift
├── SodesAudioTests
│ ├── ByteRangeSerializationTests.swift
│ ├── ByteRangeTests.swift
│ ├── DataTests.swift
│ ├── Info.plist
│ ├── ResourceLoaderSubrequestTests.swift
│ ├── URLRequestTests.swift
│ └── URLResponseTests.swift
├── SodesExample
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── EpisodeDuration.swift
│ ├── Info.plist
│ └── ViewController.swift
├── SodesFoundation
│ ├── AsyncBlockOperation.swift
│ ├── DelegatedHTTPOperation.swift
│ ├── DownloadOperation.swift
│ ├── Error.swift
│ ├── FileManager.swift
│ ├── GCD.swift
│ ├── HTTPOperation.swift
│ ├── Info.plist
│ ├── NSDateFormatter.swift
│ ├── SodesLog.swift
│ ├── Throttle.swift
│ └── WorkOperation.swift
├── SodesFoundationTests
│ ├── DateFormattingTests.swift
│ ├── DelegatedHTTPOperationTests.swift
│ └── Info.plist
└── SwiftableFileHandle
│ ├── Info.plist
│ ├── SODSwiftableFileHandle.h
│ ├── SODSwiftableFileHandle.m
│ └── SwiftableFileHandle.h
└── screenshot.png
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaredsinclair/sodes-audio-example/72548e948d767ba0b3c2894c13b664c843fbd9a6/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata/
19 |
20 | ## Other
21 | *.moved-aside
22 | *.xcuserstate
23 |
24 | ## Obj-C/Swift specific
25 | *.hmap
26 | *.ipa
27 | *.dSYM.zip
28 | *.dSYM
29 |
30 | ## Playgrounds
31 | timeline.xctimeline
32 | playground.xcworkspace
33 |
34 | # Swift Package Manager
35 | #
36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
37 | # Packages/
38 | .build/
39 |
40 | # CocoaPods
41 | #
42 | # We recommend against adding the Pods directory to your .gitignore. However
43 | # you should judge for yourself, the pros and cons are mentioned at:
44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
45 | #
46 | # Pods/
47 |
48 | # Carthage
49 | #
50 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
51 | # Carthage/Checkouts
52 |
53 | Carthage/Build
54 |
55 | # fastlane
56 | #
57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
58 | # screenshots whenever they are needed.
59 | # For more information about the recommended setup visit:
60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
61 |
62 | fastlane/report.xml
63 | fastlane/Preview.html
64 | fastlane/screenshots
65 | fastlane/test_output
66 |
--------------------------------------------------------------------------------
/Dependencies/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaredsinclair/sodes-audio-example/72548e948d767ba0b3c2894c13b664c843fbd9a6/Dependencies/.DS_Store
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/.gitignore:
--------------------------------------------------------------------------------
1 | # OS X
2 | .DS_Store
3 | .AppleDouble
4 | .LSOverride
5 | Icon
6 | ._*
7 | .Spotlight-V100
8 | .Trashes
9 |
10 | # Xcode
11 | #
12 | build/
13 | *.pbxuser
14 | !default.pbxuser
15 | *.mode1v3
16 | !default.mode1v3
17 | *.mode2v3
18 | !default.mode2v3
19 | *.perspectivev3
20 | !default.perspectivev3
21 | xcuserdata
22 | *.xccheckout
23 | *.moved-aside
24 | DerivedData
25 | *.hmap
26 | *.ipa
27 | *.xcuserstate
28 |
29 | # CocoaPods
30 | Pods
31 |
32 | # Carthage
33 | Carthage
34 |
35 | # SPM
36 | .build/
37 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/.slather.yml:
--------------------------------------------------------------------------------
1 | ci_service: travis_ci
2 | coverage_service: coveralls
3 | xcodeproj: CommonCryptoSwift.xcodeproj
4 | source_directory: Sources
5 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/.travis.yml:
--------------------------------------------------------------------------------
1 | osx_image: xcode7.3
2 | language: objective-c
3 |
4 | before_install:
5 | - brew update
6 | - if brew outdated | grep -qx carthage; then brew upgrade carthage; fi
7 | - travis_wait 35 carthage bootstrap --platform iOS,Mac
8 |
9 | script:
10 | - xcodebuild clean build -project CommonCryptoSwift.xcodeproj -scheme CommonCryptoSwift-iOS -sdk iphonesimulator
11 | - xcodebuild test -project CommonCryptoSwift.xcodeproj -scheme CommonCryptoSwift-iOS -sdk iphonesimulator
12 | - xcodebuild clean build -project CommonCryptoSwift.xcodeproj -scheme CommonCryptoSwift-Mac -sdk macosx
13 | - xcodebuild test -project CommonCryptoSwift.xcodeproj -scheme CommonCryptoSwift-Mac -sdk macosx
14 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | GitHub Issues is for reporting bugs, discussing features and general feedback in **CommonCryptoSwift**. Be sure to check our [documentation](http://cocoadocs.org/docsets/CommonCryptoSwift), [FAQ](https://github.com/onmyway133/CommonCryptoSwift/wiki/FAQ) and [past issues](https://github.com/onmyway133/CommonCryptoSwift/issues?state=closed) before opening any new issues.
2 |
3 | If you are posting about a crash in your application, a stack trace is helpful, but additional context, in the form of code and explanation, is necessary to be of any use.
4 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/CommonCryptoSwift.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 | s.name = "CommonCryptoSwift"
3 | s.summary = "CommonCrypto in Swift"
4 | s.version = "0.1.0"
5 | s.homepage = "https://github.com/onmyway133/CommonCrypto.swift"
6 | s.license = 'MIT'
7 | s.author = { "Khoa Pham" => "onmyway133@gmail.com" }
8 | s.source = {
9 | :git => "https://github.com/onmyway133/CommonCrypto.swift.git",
10 | :tag => s.version.to_s
11 | }
12 | s.social_media_url = 'https://twitter.com/onmyway133'
13 |
14 | s.ios.deployment_target = '8.0'
15 | s.osx.deployment_target = '10.9'
16 | s.tvos.deployment_target = '9.2'
17 |
18 | s.requires_arc = true
19 | s.source_files = 'Sources/**/*.swift'
20 | s.xcconfig = { 'HEADER_SEARCH_PATHS' => '$(SDKROOT)/usr/include/libxml2', 'SWIFT_INCLUDE_PATHS' => '$(PODS_ROOT)/CommonCryptoSwift/Sources/CCommonCrypto' }
21 | s.preserve_paths = 'Sources/CCommonCrypto/module.modulemap'
22 |
23 | end
24 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/CommonCryptoSwift.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/CommonCryptoSwift.xcodeproj/xcshareddata/xcschemes/CommonCryptoSwift-iOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
49 |
50 |
51 |
52 |
53 |
54 |
64 |
65 |
71 |
72 |
73 |
74 |
75 |
76 |
82 |
83 |
89 |
90 |
91 |
92 |
94 |
95 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/CommonCryptoSwift/Info-Mac.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 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSHumanReadableCopyright
24 | Copyright © 2016 Hyper Interaktiv AS. All rights reserved.
25 | NSPrincipalClass
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/CommonCryptoSwift/Info-iOS.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 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/CommonCryptoSwiftTests/Info-Mac.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 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/CommonCryptoSwiftTests/Info-iOS.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 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/CommonCryptoSwiftTests/Tests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tests.swift
3 | // CommonCryptoSwift
4 | //
5 | // Created by Khoa Pham on 07/05/16.
6 | // Copyright © 2016 Khoa Pham. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | import CommonCryptoSwift
11 |
12 | class Tests: XCTestCase {
13 |
14 | func testHash() {
15 | let string = "https://www.google.com/logos/doodles/2016/parents-day-in-korea-5757703554072576-hp2x.jpg"
16 |
17 | XCTAssertEqual(Hash.MD2(string), "e4a410bf8a43197e67a01d717bbf3557")
18 | XCTAssertEqual(Hash.MD4(string), "b263c99c66fd5703b894c020b224f71e")
19 | XCTAssertEqual(Hash.MD5(string), "0dfb10e8d2ae771b3b3ed4544139644e")
20 |
21 | XCTAssertEqual(Hash.SHA1(string), "cd99bfa31d38bb53850196b8f1daca692231def5")
22 | XCTAssertEqual(Hash.SHA224(string), "b0caf544bc75698e1efe4d55acf014ddb6ab3b5b14d312e1a5fe42d3")
23 | XCTAssertEqual(Hash.SHA256(string), "cb051d58a60b9581ff4c7ba63da07f9170f61bfbebab4a39898432ec970c3754")
24 | XCTAssertEqual(Hash.SHA384(string), "ee999f4e722bdab4534a4c3b9d33a13e37ab4bf227348d7218ef32f7483ee09f05f5f15e69e09d7d53dec46d4df16275")
25 | XCTAssertEqual(Hash.SHA512(string), "13416866022ace1b3392c0d7a7fb1e311612d3e30e9bafa9045ed9cdcd14ea4c31fb3b18d716c44ccecf8c18be5c063a0883bbc60f964ce002890fb628cf35c7")
26 | }
27 |
28 | func testHMAC() {
29 | let string = "https://www.google.com/logos/doodles/2016/parents-day-in-korea-5757703554072576-hp2x.jpg"
30 | let key = "google"
31 |
32 | XCTAssertEqual(HMAC.MD5(string, key: key), "419337f8da2e81cdf12dcb9b8e4cd76c")
33 | XCTAssertEqual(HMAC.SHA1(string, key: key), "5f4474c8872d73c1490241ab015f6c672c6dcdc8")
34 | XCTAssertEqual(HMAC.SHA224(string, key: key), "82a903faa4f93c528f490c699c9bfef0c0ef8a3498dd677cfab0a71e")
35 | XCTAssertEqual(HMAC.SHA256(string, key: key), "a8e314bf001c5ea640c374582f5a27d9d29a39cb1b1c729748bad5365dfb0632")
36 | XCTAssertEqual(HMAC.SHA384(string, key: key), "afa2026322d2843e564a9d88e2a03a5a4f75581386217052d952bdda7a7af6a48ba4e0b927229cdec46cdf7e5ffa9ee3")
37 | XCTAssertEqual(HMAC.SHA512(string, key: key), "d75af2a8f76dd22b3e7a52f84593e3c9d56065a675f5e96ecf3e1354d6dba05f357418fbfbd3af0491e69f548bb4200600d139d601741c6c9eb37952e2e38d7a")
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/CommonCryptoSwiftDemo.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 97F29A739B5DAF048446AB43 /* Pods_CommonCryptoSwiftDemo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C8E2B9C49E327C0C4BC5B2C /* Pods_CommonCryptoSwiftDemo.framework */; };
11 | D5C7F74E1C3BC9CE008CDDBA /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D5C7F74C1C3BC9CE008CDDBA /* LaunchScreen.storyboard */; };
12 | D5C7F75B1C3BCA1E008CDDBA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D5C7F7571C3BCA1E008CDDBA /* Assets.xcassets */; };
13 | D5C7F75C1C3BCA1E008CDDBA /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C7F7591C3BCA1E008CDDBA /* AppDelegate.swift */; };
14 | D5C7F75D1C3BCA1E008CDDBA /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C7F75A1C3BCA1E008CDDBA /* ViewController.swift */; };
15 | /* End PBXBuildFile section */
16 |
17 | /* Begin PBXFileReference section */
18 | 0CA6DC52BD90B876037F7603 /* Pods-CommonCryptoSwiftDemo.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CommonCryptoSwiftDemo.release.xcconfig"; path = "Pods/Target Support Files/Pods-CommonCryptoSwiftDemo/Pods-CommonCryptoSwiftDemo.release.xcconfig"; sourceTree = ""; };
19 | 2C8E2B9C49E327C0C4BC5B2C /* Pods_CommonCryptoSwiftDemo.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_CommonCryptoSwiftDemo.framework; sourceTree = BUILT_PRODUCTS_DIR; };
20 | 5D55BF7BF84BEDD023F53C68 /* Pods-CommonCryptoSwiftDemo.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-CommonCryptoSwiftDemo.debug.xcconfig"; path = "Pods/Target Support Files/Pods-CommonCryptoSwiftDemo/Pods-CommonCryptoSwiftDemo.debug.xcconfig"; sourceTree = ""; };
21 | D5C7F7401C3BC9CE008CDDBA /* CommonCryptoSwiftDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CommonCryptoSwiftDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
22 | D5C7F74D1C3BC9CE008CDDBA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
23 | D5C7F74F1C3BC9CE008CDDBA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
24 | D5C7F7571C3BCA1E008CDDBA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
25 | D5C7F7591C3BCA1E008CDDBA /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
26 | D5C7F75A1C3BCA1E008CDDBA /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
27 | /* End PBXFileReference section */
28 |
29 | /* Begin PBXFrameworksBuildPhase section */
30 | D5C7F73D1C3BC9CE008CDDBA /* Frameworks */ = {
31 | isa = PBXFrameworksBuildPhase;
32 | buildActionMask = 2147483647;
33 | files = (
34 | 97F29A739B5DAF048446AB43 /* Pods_CommonCryptoSwiftDemo.framework in Frameworks */,
35 | );
36 | runOnlyForDeploymentPostprocessing = 0;
37 | };
38 | /* End PBXFrameworksBuildPhase section */
39 |
40 | /* Begin PBXGroup section */
41 | 2E7CE5304D233567E667E08C /* Pods */ = {
42 | isa = PBXGroup;
43 | children = (
44 | 5D55BF7BF84BEDD023F53C68 /* Pods-CommonCryptoSwiftDemo.debug.xcconfig */,
45 | 0CA6DC52BD90B876037F7603 /* Pods-CommonCryptoSwiftDemo.release.xcconfig */,
46 | );
47 | name = Pods;
48 | sourceTree = "";
49 | };
50 | 42CC7077ACF00910DFC948C7 /* Frameworks */ = {
51 | isa = PBXGroup;
52 | children = (
53 | 2C8E2B9C49E327C0C4BC5B2C /* Pods_CommonCryptoSwiftDemo.framework */,
54 | );
55 | name = Frameworks;
56 | sourceTree = "";
57 | };
58 | D5C7F7371C3BC9CE008CDDBA = {
59 | isa = PBXGroup;
60 | children = (
61 | D5C7F7421C3BC9CE008CDDBA /* CommonCryptoSwiftDemo */,
62 | D5C7F7411C3BC9CE008CDDBA /* Products */,
63 | 2E7CE5304D233567E667E08C /* Pods */,
64 | 42CC7077ACF00910DFC948C7 /* Frameworks */,
65 | );
66 | sourceTree = "";
67 | };
68 | D5C7F7411C3BC9CE008CDDBA /* Products */ = {
69 | isa = PBXGroup;
70 | children = (
71 | D5C7F7401C3BC9CE008CDDBA /* CommonCryptoSwiftDemo.app */,
72 | );
73 | name = Products;
74 | sourceTree = "";
75 | };
76 | D5C7F7421C3BC9CE008CDDBA /* CommonCryptoSwiftDemo */ = {
77 | isa = PBXGroup;
78 | children = (
79 | D5C7F7561C3BCA1E008CDDBA /* Resources */,
80 | D5C7F7581C3BCA1E008CDDBA /* Sources */,
81 | D5C7F7551C3BC9EA008CDDBA /* Supporting Files */,
82 | );
83 | path = CommonCryptoSwiftDemo;
84 | sourceTree = "";
85 | };
86 | D5C7F7551C3BC9EA008CDDBA /* Supporting Files */ = {
87 | isa = PBXGroup;
88 | children = (
89 | D5C7F74C1C3BC9CE008CDDBA /* LaunchScreen.storyboard */,
90 | D5C7F74F1C3BC9CE008CDDBA /* Info.plist */,
91 | );
92 | name = "Supporting Files";
93 | sourceTree = "";
94 | };
95 | D5C7F7561C3BCA1E008CDDBA /* Resources */ = {
96 | isa = PBXGroup;
97 | children = (
98 | D5C7F7571C3BCA1E008CDDBA /* Assets.xcassets */,
99 | );
100 | path = Resources;
101 | sourceTree = "";
102 | };
103 | D5C7F7581C3BCA1E008CDDBA /* Sources */ = {
104 | isa = PBXGroup;
105 | children = (
106 | D5C7F7591C3BCA1E008CDDBA /* AppDelegate.swift */,
107 | D5C7F75A1C3BCA1E008CDDBA /* ViewController.swift */,
108 | );
109 | path = Sources;
110 | sourceTree = "";
111 | };
112 | /* End PBXGroup section */
113 |
114 | /* Begin PBXNativeTarget section */
115 | D5C7F73F1C3BC9CE008CDDBA /* CommonCryptoSwiftDemo */ = {
116 | isa = PBXNativeTarget;
117 | buildConfigurationList = D5C7F7521C3BC9CE008CDDBA /* Build configuration list for PBXNativeTarget "CommonCryptoSwiftDemo" */;
118 | buildPhases = (
119 | C7D3FFB9DCD3A4C6AA2DA385 /* 📦 Check Pods Manifest.lock */,
120 | D5C7F73C1C3BC9CE008CDDBA /* Sources */,
121 | D5C7F73D1C3BC9CE008CDDBA /* Frameworks */,
122 | D5C7F73E1C3BC9CE008CDDBA /* Resources */,
123 | 68E6206C43FDCED318C2C79A /* 📦 Embed Pods Frameworks */,
124 | 2B4CDA8F16A1BF04F2805683 /* 📦 Copy Pods Resources */,
125 | );
126 | buildRules = (
127 | );
128 | dependencies = (
129 | );
130 | name = CommonCryptoSwiftDemo;
131 | productName = CommonCryptoSwiftDemo;
132 | productReference = D5C7F7401C3BC9CE008CDDBA /* CommonCryptoSwiftDemo.app */;
133 | productType = "com.apple.product-type.application";
134 | };
135 | /* End PBXNativeTarget section */
136 |
137 | /* Begin PBXProject section */
138 | D5C7F7381C3BC9CE008CDDBA /* Project object */ = {
139 | isa = PBXProject;
140 | attributes = {
141 | LastSwiftUpdateCheck = 0720;
142 | LastUpgradeCheck = 0720;
143 | ORGANIZATIONNAME = "Hyper Interaktiv AS";
144 | TargetAttributes = {
145 | D5C7F73F1C3BC9CE008CDDBA = {
146 | CreatedOnToolsVersion = 7.2;
147 | };
148 | };
149 | };
150 | buildConfigurationList = D5C7F73B1C3BC9CE008CDDBA /* Build configuration list for PBXProject "CommonCryptoSwiftDemo" */;
151 | compatibilityVersion = "Xcode 3.2";
152 | developmentRegion = English;
153 | hasScannedForEncodings = 0;
154 | knownRegions = (
155 | en,
156 | Base,
157 | );
158 | mainGroup = D5C7F7371C3BC9CE008CDDBA;
159 | productRefGroup = D5C7F7411C3BC9CE008CDDBA /* Products */;
160 | projectDirPath = "";
161 | projectRoot = "";
162 | targets = (
163 | D5C7F73F1C3BC9CE008CDDBA /* CommonCryptoSwiftDemo */,
164 | );
165 | };
166 | /* End PBXProject section */
167 |
168 | /* Begin PBXResourcesBuildPhase section */
169 | D5C7F73E1C3BC9CE008CDDBA /* Resources */ = {
170 | isa = PBXResourcesBuildPhase;
171 | buildActionMask = 2147483647;
172 | files = (
173 | D5C7F75B1C3BCA1E008CDDBA /* Assets.xcassets in Resources */,
174 | D5C7F74E1C3BC9CE008CDDBA /* LaunchScreen.storyboard in Resources */,
175 | );
176 | runOnlyForDeploymentPostprocessing = 0;
177 | };
178 | /* End PBXResourcesBuildPhase section */
179 |
180 | /* Begin PBXShellScriptBuildPhase section */
181 | 2B4CDA8F16A1BF04F2805683 /* 📦 Copy Pods Resources */ = {
182 | isa = PBXShellScriptBuildPhase;
183 | buildActionMask = 2147483647;
184 | files = (
185 | );
186 | inputPaths = (
187 | );
188 | name = "📦 Copy Pods Resources";
189 | outputPaths = (
190 | );
191 | runOnlyForDeploymentPostprocessing = 0;
192 | shellPath = /bin/sh;
193 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-CommonCryptoSwiftDemo/Pods-CommonCryptoSwiftDemo-resources.sh\"\n";
194 | showEnvVarsInLog = 0;
195 | };
196 | 68E6206C43FDCED318C2C79A /* 📦 Embed Pods Frameworks */ = {
197 | isa = PBXShellScriptBuildPhase;
198 | buildActionMask = 2147483647;
199 | files = (
200 | );
201 | inputPaths = (
202 | );
203 | name = "📦 Embed Pods Frameworks";
204 | outputPaths = (
205 | );
206 | runOnlyForDeploymentPostprocessing = 0;
207 | shellPath = /bin/sh;
208 | shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-CommonCryptoSwiftDemo/Pods-CommonCryptoSwiftDemo-frameworks.sh\"\n";
209 | showEnvVarsInLog = 0;
210 | };
211 | C7D3FFB9DCD3A4C6AA2DA385 /* 📦 Check Pods Manifest.lock */ = {
212 | isa = PBXShellScriptBuildPhase;
213 | buildActionMask = 2147483647;
214 | files = (
215 | );
216 | inputPaths = (
217 | );
218 | name = "📦 Check Pods Manifest.lock";
219 | outputPaths = (
220 | );
221 | runOnlyForDeploymentPostprocessing = 0;
222 | shellPath = /bin/sh;
223 | shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [[ $? != 0 ]] ; then\n cat << EOM\nerror: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\nEOM\n exit 1\nfi\n";
224 | showEnvVarsInLog = 0;
225 | };
226 | /* End PBXShellScriptBuildPhase section */
227 |
228 | /* Begin PBXSourcesBuildPhase section */
229 | D5C7F73C1C3BC9CE008CDDBA /* Sources */ = {
230 | isa = PBXSourcesBuildPhase;
231 | buildActionMask = 2147483647;
232 | files = (
233 | D5C7F75D1C3BCA1E008CDDBA /* ViewController.swift in Sources */,
234 | D5C7F75C1C3BCA1E008CDDBA /* AppDelegate.swift in Sources */,
235 | );
236 | runOnlyForDeploymentPostprocessing = 0;
237 | };
238 | /* End PBXSourcesBuildPhase section */
239 |
240 | /* Begin PBXVariantGroup section */
241 | D5C7F74C1C3BC9CE008CDDBA /* LaunchScreen.storyboard */ = {
242 | isa = PBXVariantGroup;
243 | children = (
244 | D5C7F74D1C3BC9CE008CDDBA /* Base */,
245 | );
246 | name = LaunchScreen.storyboard;
247 | sourceTree = "";
248 | };
249 | /* End PBXVariantGroup section */
250 |
251 | /* Begin XCBuildConfiguration section */
252 | D5C7F7501C3BC9CE008CDDBA /* Debug */ = {
253 | isa = XCBuildConfiguration;
254 | buildSettings = {
255 | ALWAYS_SEARCH_USER_PATHS = NO;
256 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
257 | CLANG_CXX_LIBRARY = "libc++";
258 | CLANG_ENABLE_MODULES = YES;
259 | CLANG_ENABLE_OBJC_ARC = YES;
260 | CLANG_WARN_BOOL_CONVERSION = YES;
261 | CLANG_WARN_CONSTANT_CONVERSION = YES;
262 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
263 | CLANG_WARN_EMPTY_BODY = YES;
264 | CLANG_WARN_ENUM_CONVERSION = YES;
265 | CLANG_WARN_INT_CONVERSION = YES;
266 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
267 | CLANG_WARN_UNREACHABLE_CODE = YES;
268 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
269 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
270 | COPY_PHASE_STRIP = NO;
271 | DEBUG_INFORMATION_FORMAT = dwarf;
272 | ENABLE_STRICT_OBJC_MSGSEND = YES;
273 | ENABLE_TESTABILITY = YES;
274 | GCC_C_LANGUAGE_STANDARD = gnu99;
275 | GCC_DYNAMIC_NO_PIC = NO;
276 | GCC_NO_COMMON_BLOCKS = YES;
277 | GCC_OPTIMIZATION_LEVEL = 0;
278 | GCC_PREPROCESSOR_DEFINITIONS = (
279 | "DEBUG=1",
280 | "$(inherited)",
281 | );
282 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
283 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
284 | GCC_WARN_UNDECLARED_SELECTOR = YES;
285 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
286 | GCC_WARN_UNUSED_FUNCTION = YES;
287 | GCC_WARN_UNUSED_VARIABLE = YES;
288 | IPHONEOS_DEPLOYMENT_TARGET = 9.2;
289 | MTL_ENABLE_DEBUG_INFO = YES;
290 | ONLY_ACTIVE_ARCH = YES;
291 | SDKROOT = iphoneos;
292 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
293 | };
294 | name = Debug;
295 | };
296 | D5C7F7511C3BC9CE008CDDBA /* Release */ = {
297 | isa = XCBuildConfiguration;
298 | buildSettings = {
299 | ALWAYS_SEARCH_USER_PATHS = NO;
300 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
301 | CLANG_CXX_LIBRARY = "libc++";
302 | CLANG_ENABLE_MODULES = YES;
303 | CLANG_ENABLE_OBJC_ARC = YES;
304 | CLANG_WARN_BOOL_CONVERSION = YES;
305 | CLANG_WARN_CONSTANT_CONVERSION = YES;
306 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
307 | CLANG_WARN_EMPTY_BODY = YES;
308 | CLANG_WARN_ENUM_CONVERSION = YES;
309 | CLANG_WARN_INT_CONVERSION = YES;
310 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
311 | CLANG_WARN_UNREACHABLE_CODE = YES;
312 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
313 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
314 | COPY_PHASE_STRIP = NO;
315 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
316 | ENABLE_NS_ASSERTIONS = NO;
317 | ENABLE_STRICT_OBJC_MSGSEND = YES;
318 | GCC_C_LANGUAGE_STANDARD = gnu99;
319 | GCC_NO_COMMON_BLOCKS = YES;
320 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
321 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
322 | GCC_WARN_UNDECLARED_SELECTOR = YES;
323 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
324 | GCC_WARN_UNUSED_FUNCTION = YES;
325 | GCC_WARN_UNUSED_VARIABLE = YES;
326 | IPHONEOS_DEPLOYMENT_TARGET = 9.2;
327 | MTL_ENABLE_DEBUG_INFO = NO;
328 | SDKROOT = iphoneos;
329 | VALIDATE_PRODUCT = YES;
330 | };
331 | name = Release;
332 | };
333 | D5C7F7531C3BC9CE008CDDBA /* Debug */ = {
334 | isa = XCBuildConfiguration;
335 | baseConfigurationReference = 5D55BF7BF84BEDD023F53C68 /* Pods-CommonCryptoSwiftDemo.debug.xcconfig */;
336 | buildSettings = {
337 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
338 | INFOPLIST_FILE = CommonCryptoSwiftDemo/Info.plist;
339 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
340 | PRODUCT_BUNDLE_IDENTIFIER = com.fantageek.CommonCryptoSwift.CommonCryptoSwiftDemo;
341 | PRODUCT_NAME = "$(TARGET_NAME)";
342 | };
343 | name = Debug;
344 | };
345 | D5C7F7541C3BC9CE008CDDBA /* Release */ = {
346 | isa = XCBuildConfiguration;
347 | baseConfigurationReference = 0CA6DC52BD90B876037F7603 /* Pods-CommonCryptoSwiftDemo.release.xcconfig */;
348 | buildSettings = {
349 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
350 | INFOPLIST_FILE = CommonCryptoSwiftDemo/Info.plist;
351 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
352 | PRODUCT_BUNDLE_IDENTIFIER = com.fantageek.CommonCryptoSwift.CommonCryptoSwiftDemo;
353 | PRODUCT_NAME = "$(TARGET_NAME)";
354 | };
355 | name = Release;
356 | };
357 | /* End XCBuildConfiguration section */
358 |
359 | /* Begin XCConfigurationList section */
360 | D5C7F73B1C3BC9CE008CDDBA /* Build configuration list for PBXProject "CommonCryptoSwiftDemo" */ = {
361 | isa = XCConfigurationList;
362 | buildConfigurations = (
363 | D5C7F7501C3BC9CE008CDDBA /* Debug */,
364 | D5C7F7511C3BC9CE008CDDBA /* Release */,
365 | );
366 | defaultConfigurationIsVisible = 0;
367 | defaultConfigurationName = Release;
368 | };
369 | D5C7F7521C3BC9CE008CDDBA /* Build configuration list for PBXNativeTarget "CommonCryptoSwiftDemo" */ = {
370 | isa = XCConfigurationList;
371 | buildConfigurations = (
372 | D5C7F7531C3BC9CE008CDDBA /* Debug */,
373 | D5C7F7541C3BC9CE008CDDBA /* Release */,
374 | );
375 | defaultConfigurationIsVisible = 0;
376 | defaultConfigurationName = Release;
377 | };
378 | /* End XCConfigurationList section */
379 | };
380 | rootObject = D5C7F7381C3BC9CE008CDDBA /* Project object */;
381 | }
382 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/CommonCryptoSwiftDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/CommonCryptoSwiftDemo.xcodeproj/xcshareddata/xcschemes/CommonCryptoSwiftDemo.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/CommonCryptoSwiftDemo.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/CommonCryptoSwiftDemo/Base.lproj/LaunchScreen.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 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/CommonCryptoSwiftDemo/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 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | UILaunchStoryboardName
26 | LaunchScreen
27 | UIRequiredDeviceCapabilities
28 |
29 | armv7
30 |
31 | UISupportedInterfaceOrientations
32 |
33 | UIInterfaceOrientationPortrait
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/CommonCryptoSwiftDemo/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/CommonCryptoSwiftDemo/Sources/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import CommonCryptoSwift
3 |
4 | @UIApplicationMain
5 | class AppDelegate: UIResponder, UIApplicationDelegate {
6 |
7 | var window: UIWindow?
8 |
9 | lazy var navigationController: UINavigationController = { [unowned self] in
10 | let controller = UINavigationController(rootViewController: self.viewController)
11 | return controller
12 | }()
13 |
14 | lazy var viewController: ViewController = {
15 | let controller = ViewController()
16 | return controller
17 | }()
18 |
19 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
20 | window = UIWindow(frame: UIScreen.mainScreen().bounds)
21 | window?.rootViewController = navigationController
22 | window?.makeKeyAndVisible()
23 |
24 | return true
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/CommonCryptoSwiftDemo/Sources/ViewController.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import CommonCryptoSwift
3 |
4 | class ViewController: UIViewController {
5 |
6 | override func viewDidLoad() {
7 | super.viewDidLoad()
8 |
9 | assert(Hash.MD5("https://www.google.com/logos/doodles/2016/parents-day-in-korea-5757703554072576-hp2x.jpg") == "0dfb10e8d2ae771b3b3ed4544139644e")
10 | print(Hash.MD5("https://www.google.com/logos/doodles/2016/parents-day-in-korea-5757703554072576-hp2x.jpg"))
11 | }
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/Podfile:
--------------------------------------------------------------------------------
1 | use_frameworks!
2 |
3 | platform :ios, '8.0'
4 |
5 | target 'CommonCryptoSwiftDemo' do
6 | pod 'CommonCryptoSwift', :git => 'https://github.com/onmyway133/CommonCrypto.swift'
7 | end
8 |
9 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Example/CommonCryptoSwiftDemo/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - CommonCryptoSwift (0.1.0)
3 |
4 | DEPENDENCIES:
5 | - CommonCryptoSwift (from `https://github.com/onmyway133/CommonCrypto.swift`)
6 |
7 | EXTERNAL SOURCES:
8 | CommonCryptoSwift:
9 | :git: https://github.com/onmyway133/CommonCrypto.swift
10 |
11 | CHECKOUT OPTIONS:
12 | CommonCryptoSwift:
13 | :commit: c4d758ec195bb38284cb2149dc6dee21ad5dd1e6
14 | :git: https://github.com/onmyway133/CommonCrypto.swift
15 |
16 | SPEC CHECKSUMS:
17 | CommonCryptoSwift: 3248a2412fac3fced7ed51c62b3a9d1932e21823
18 |
19 | PODFILE CHECKSUM: a2e5ffbdf0fc0edb82758b54102d3f6af53280f9
20 |
21 | COCOAPODS: 1.0.0.beta.8
22 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/LICENSE.md:
--------------------------------------------------------------------------------
1 | Licensed under the **MIT** license
2 |
3 | > Copyright (c) 2015 Khoa Pham
4 | >
5 | > Permission is hereby granted, free of charge, to any person obtaining
6 | > a copy of this software and associated documentation files (the
7 | > "Software"), to deal in the Software without restriction, including
8 | > without limitation the rights to use, copy, modify, merge, publish,
9 | > distribute, sublicense, and/or sell copies of the Software, and to
10 | > permit persons to whom the Software is furnished to do so, subject to
11 | > the following conditions:
12 | >
13 | > The above copyright notice and this permission notice shall be
14 | > included in all copies or substantial portions of the Software.
15 | >
16 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | > EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | > MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | > IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20 | > CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21 | > TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22 | > SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Playground-Mac.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | // CommonCryptoSwift Mac Playground
2 |
3 | import Cocoa
4 | import CommonCryptoSwift
5 |
6 | var str = "Hello, playground"
7 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Playground-Mac.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Playground-iOS.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | // CommonCryptoSwift iOS Playground
2 |
3 | import UIKit
4 | import CommonCryptoSwift
5 |
6 | var str = "Hello, playground"
7 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Playground-iOS.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/README.md:
--------------------------------------------------------------------------------
1 | # CommonCrypto.swift
2 | CommonCrypto in Swift
3 |
4 | [](https://travis-ci.org/onmyway133/CommonCryptoSwift)
5 | [](http://cocoadocs.org/docsets/CommonCryptoSwift)
6 | [](https://github.com/Carthage/Carthage)
7 | [](http://cocoadocs.org/docsets/CommonCryptoSwift)
8 | [](http://cocoadocs.org/docsets/CommonCryptoSwift)
9 |
10 | 
11 |
12 | ## Features
13 |
14 | - Work on NSData, String
15 | - Message Digest, SHA, HMAC
16 | - Hash Algorithm: MD2, MD4, MD5, SHA1, SHA256, SHA224, SHA384, SHA512
17 |
18 | ```swift
19 | Hash.MD5("https://www.google.com/logos/doodles/2016/parents-day-in-korea-5757703554072576-hp2x.jpg") // 0dfb10e8d2ae771b3b3ed4544139644e
20 | Hash.SHA246("https://www.google.com/logos/doodles/2016/parents-day-in-korea-5757703554072576-hp2x.jpg") // cb051d58a60b9581ff4c7ba63da07f9170f61bfbebab4a39898432ec970c3754
21 | HMAC.SHA1("https://www.google.com/logos/doodles/2016/parents-day-in-korea-5757703554072576-hp2x.jpg", key: "google") // 5f4474c8872d73c1490241ab015f6c672c6dcdc8
22 | ```
23 |
24 | ## Installation
25 |
26 | **CommonCryptoSwift** is available through [CocoaPods](http://cocoapods.org). To install
27 | it, simply add the following line to your Podfile:
28 |
29 | ```ruby
30 | pod 'CommonCryptoSwift', git: 'https://github.com/onmyway133/CommonCrypto.swift'
31 | ```
32 |
33 | **CommonCryptoSwift** is also available through [Carthage](https://github.com/Carthage/Carthage).
34 | To install just write into your Cartfile:
35 |
36 | ```ruby
37 | github "onmyway133/CommonCrypto.swift"
38 | ```
39 |
40 | ## Author
41 |
42 | Khoa Pham, onmyway133@gmail.com
43 |
44 | ## Contributing
45 |
46 | We would love you to contribute to **CommonCryptoSwift**, check the [CONTRIBUTING](https://github.com/onmyway133/CommonCryptoSwift/blob/master/CONTRIBUTING.md) file for more info.
47 |
48 | ## License
49 |
50 | **CommonCryptoSwift** is available under the MIT license. See the [LICENSE](https://github.com/onmyway133/CommonCryptoSwift/blob/master/LICENSE.md) file for more info.
51 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Screenshots/Banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaredsinclair/sodes-audio-example/72548e948d767ba0b3c2894c13b664c843fbd9a6/Dependencies/CommonCrypto.swift/Screenshots/Banner.png
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Sources/CCommonCrypto/module.modulemap:
--------------------------------------------------------------------------------
1 | module CCommonCrypto {
2 | header "/usr/include/CommonCrypto/CommonCrypto.h"
3 | export *
4 | }
5 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Sources/Crypto.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Crypto.swift
3 | // CommonCryptoSwift
4 | //
5 | // Created by Khoa Pham on 08/05/16.
6 | // Copyright © 2016 Fantageek. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CCommonCrypto
11 |
12 | typealias DigestMethod = (_ data: UnsafeRawPointer, _ len: CC_LONG, _ md: UnsafeMutablePointer) -> UnsafeMutablePointer!
13 |
14 | struct Crypto {
15 | let length: Int32
16 | let method: DigestMethod
17 | let HMACAlgorithm: CCHmacAlgorithm?
18 |
19 | static let MD2 = Crypto(length: CC_MD2_DIGEST_LENGTH, method: CC_MD2,
20 | HMACAlgorithm: nil)
21 | static let MD4 = Crypto(length: CC_MD4_DIGEST_LENGTH, method: CC_MD4,
22 | HMACAlgorithm: nil)
23 | static let MD5 = Crypto(length: CC_MD5_DIGEST_LENGTH, method: CC_MD5,
24 | HMACAlgorithm: CCHmacAlgorithm(kCCHmacAlgMD5))
25 | static let SHA1 = Crypto(length: CC_SHA1_DIGEST_LENGTH, method: CC_SHA1,
26 | HMACAlgorithm: CCHmacAlgorithm(kCCHmacAlgSHA1))
27 | static let SHA224 = Crypto(length: CC_SHA224_DIGEST_LENGTH, method: CC_SHA224,
28 | HMACAlgorithm: CCHmacAlgorithm(kCCHmacAlgSHA224))
29 | static let SHA256 = Crypto(length: CC_SHA256_DIGEST_LENGTH, method: CC_SHA256,
30 | HMACAlgorithm: CCHmacAlgorithm(kCCHmacAlgSHA256))
31 | static let SHA384 = Crypto(length: CC_SHA384_DIGEST_LENGTH, method: CC_SHA384,
32 | HMACAlgorithm: CCHmacAlgorithm(kCCHmacAlgSHA384))
33 | static let SHA512 = Crypto(length: CC_SHA512_DIGEST_LENGTH, method: CC_SHA512,
34 | HMACAlgorithm: CCHmacAlgorithm(kCCHmacAlgSHA512))
35 | }
36 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Sources/HMAC.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HMAC.swift
3 | // CommonCryptoSwift
4 | //
5 | // Created by Khoa Pham on 08/05/16.
6 | // Copyright © 2016 Fantageek. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CCommonCrypto
11 |
12 | public struct HMAC {
13 |
14 | // MARK: - NSData
15 |
16 | public static func MD5(data: NSData, key: NSData) -> NSData? {
17 | return HMAC.generate(data: data, key: key, crypto: .MD5)
18 | }
19 |
20 | public static func SHA1(data: NSData, key: NSData) -> NSData? {
21 | return HMAC.generate(data: data, key: key, crypto: .SHA1)
22 | }
23 |
24 | public static func SHA224(data: NSData, key: NSData) -> NSData? {
25 | return HMAC.generate(data: data, key: key, crypto: .SHA224)
26 | }
27 |
28 | public static func SHA256(data: NSData, key: NSData) -> NSData? {
29 | return HMAC.generate(data: data, key: key, crypto: .SHA256)
30 | }
31 |
32 | public static func SHA384(data: NSData, key: NSData) -> NSData? {
33 | return HMAC.generate(data: data, key: key, crypto: .SHA384)
34 | }
35 |
36 | public static func SHA512(data: NSData, key: NSData) -> NSData? {
37 | return HMAC.generate(data: data, key: key, crypto: .SHA512)
38 | }
39 |
40 | static func generate(data: NSData, key: NSData, crypto: Crypto) -> NSData? {
41 | guard let HMACAlgorithm = crypto.HMACAlgorithm else { return nil }
42 |
43 | // Can also use UnsafeMutablePointer.alloc(Int(crypto.length))
44 | var buffer = Array(repeating: 0, count: Int(crypto.length))
45 | CCHmac(HMACAlgorithm, key.bytes, key.length, data.bytes, data.length, &buffer)
46 |
47 | return NSData(bytes: buffer, length: Int(crypto.length))
48 | }
49 |
50 | // MARK: - String
51 |
52 | public static func MD5(string: String, key: String) -> String? {
53 | return HMAC.generate(string: string, key: key, crypto: .MD5)
54 | }
55 |
56 | public static func SHA1(string: String, key: String) -> String? {
57 | return HMAC.generate(string: string, key: key, crypto: .SHA1)
58 | }
59 |
60 | public static func SHA224(string: String, key: String) -> String? {
61 | return HMAC.generate(string: string, key: key, crypto: .SHA224)
62 | }
63 |
64 | public static func SHA256(string: String, key: String) -> String? {
65 | return HMAC.generate(string: string, key: key, crypto: .SHA256)
66 | }
67 |
68 | public static func SHA384(string: String, key: String) -> String? {
69 | return HMAC.generate(string: string, key: key, crypto: .SHA384)
70 | }
71 |
72 | public static func SHA512(string: String, key: String) -> String? {
73 | return HMAC.generate(string: string, key: key, crypto: .SHA512)
74 | }
75 |
76 | static func generate(string: String, key: String, crypto: Crypto) -> String? {
77 | guard let data = string.data(using: String.Encoding.utf8),
78 | let keyData = key.data(using: String.Encoding.utf8)
79 | else { return nil }
80 |
81 | return HMAC.generate(data: data as NSData, key: keyData as NSData, crypto: crypto)?.hexString
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Sources/Hash.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Hash.swift
3 | // CommonCryptoSwift
4 | //
5 | // Created by Khoa Pham on 07/05/16.
6 | // Copyright © 2016 Fantageek. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import CCommonCrypto
11 |
12 | public struct Hash {
13 |
14 | // MARK: - NSData
15 |
16 | public static func MD2(data: NSData) -> NSData {
17 | return Hash.hash(data: data, crypto: .MD2)
18 | }
19 |
20 | public static func MD4(data: NSData) -> NSData {
21 | return Hash.hash(data: data, crypto: .MD4)
22 | }
23 |
24 | public static func MD5(data: NSData) -> NSData {
25 | return Hash.hash(data: data, crypto: .MD5)
26 | }
27 |
28 | public static func SHA1(data: NSData) -> NSData {
29 | return Hash.hash(data: data, crypto: .SHA1)
30 | }
31 |
32 | public static func SHA224(data: NSData) -> NSData {
33 | return Hash.hash(data: data, crypto: .SHA224)
34 | }
35 |
36 | public static func SHA256(data: NSData) -> NSData {
37 | return Hash.hash(data: data, crypto: .SHA256)
38 | }
39 |
40 | public static func SHA384(data: NSData) -> NSData {
41 | return Hash.hash(data: data, crypto: .SHA384)
42 | }
43 |
44 | public static func SHA512(data: NSData) -> NSData {
45 | return Hash.hash(data: data, crypto: .SHA512)
46 | }
47 |
48 | static func hash(data: NSData, crypto: Crypto) -> NSData {
49 | var buffer = Array(repeating: 0, count: Int(crypto.length))
50 | let _ = crypto.method(data.bytes, UInt32(data.length), &buffer)
51 |
52 | return NSData(bytes: buffer, length: buffer.count)
53 | }
54 |
55 | // MARK: - String
56 |
57 | public static func MD2(_ string: String) -> String? {
58 | return Hash.hash(string: string, crypto: .MD2)
59 | }
60 |
61 | public static func MD4(_ string: String) -> String? {
62 | return Hash.hash(string: string, crypto: .MD4)
63 | }
64 |
65 | public static func MD5(_ string: String) -> String? {
66 | return Hash.hash(string: string, crypto: .MD5)
67 | }
68 |
69 | public static func SHA1(_ string: String) -> String? {
70 | return Hash.hash(string: string, crypto: .SHA1)
71 | }
72 |
73 | public static func SHA224(_ string: String) -> String? {
74 | return Hash.hash(string: string, crypto: .SHA224)
75 | }
76 |
77 | public static func SHA256(_ string: String) -> String? {
78 | return Hash.hash(string: string, crypto: .SHA256)
79 | }
80 |
81 | public static func SHA384(_ string: String) -> String? {
82 | return Hash.hash(string: string, crypto: .SHA384)
83 | }
84 |
85 | public static func SHA512(_ string: String) -> String? {
86 | return Hash.hash(string: string, crypto: .SHA512)
87 | }
88 |
89 | static func hash(string: String, crypto: Crypto) -> String? {
90 | guard let data = string.data(using: String.Encoding.utf8) else { return nil }
91 |
92 | return Hash.hash(data: data as NSData, crypto: crypto).hexString
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/Dependencies/CommonCrypto.swift/Sources/NSData+Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSData+Extensions.swift
3 | // CommonCryptoSwift
4 | //
5 | // Created by Khoa Pham on 08/05/16.
6 | // Copyright © 2016 Fantageek. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | public extension NSData {
12 |
13 | var hexString: String {
14 | var result = ""
15 |
16 | var bytes = [UInt8](repeating: 0, count: length)
17 | getBytes(&bytes, length: length)
18 |
19 | for byte in bytes {
20 | result += String(format: "%02x", UInt(byte))
21 | }
22 |
23 | return result
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Jared Sinclair
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # sodes-audio-example
2 |
3 | An example AVAssetResourceLoaderDelegate implementation. A variation of this will be used in **’sodes**, a podcast app I'm working on.
4 |
5 | This repo accompanies a blog post [which can be found here](http://blog.jaredsinclair.com/post/149892449150/avassetresourceloaderdelegate).
6 |
7 | You are welcome to use this code as allowed under the generous terms of the MIT License, but **this code is not intended to be used as a re-usable library**. It's highly optimized for the needs of my particular app. I'm sharing it here for the benefit of anyone who's looking for an example of how to write an AVAssetResourceLoaderDelegate implementation.
8 |
9 | ## What It Does
10 |
11 | Contains an example implementation of an AVAssetResourceLoaderDelegate which downloads the requested byte ranges to a "scratch file" of locally-cached byte ranges. It also re-uses previously-downloaded byte ranges from that scratch file to service future requests that overlap the downloaded byte ranges, both during the current app session and in future sessions. This helps limit the number of times the same bytes are downloaded when streaming a podcast episode over more than one app session. Ideally each byte should never be downloaded more than once.
12 |
13 | When a request for a byte range is sent to the resource loader delegate, an array of "subrequests" is formed which are either scratch file requests or network requests. Scratch file requests read the data from existing byte ranges in the scratch file which have already been downloaded. Network requests are made for any gaps in the scratch file. The results of network requests are both passed to the AVAssetResourceLoader and written to the scratch file to be re-used later if the need arises.
14 |
15 | ## TL;DR Files
16 |
17 | - [ResourceLoaderDelegate.swift](https://github.com/jaredsinclair/sodes-audio-example/blob/master/Sodes/SodesAudio/ResourceLoaderDelegate.swift)
18 | - [DataRequestLoader.swift](https://github.com/jaredsinclair/sodes-audio-example/blob/master/Sodes/SodesAudio/DataRequestLoader.swift)
19 | - [ResourceLoaderSubrequest.swift](https://github.com/jaredsinclair/sodes-audio-example/blob/master/Sodes/SodesAudio/ResourceLoaderSubrequest.swift)
20 | - [PlaybackController.swift](https://github.com/jaredsinclair/sodes-audio-example/blob/master/Sodes/SodesAudio/PlaybackController.swift)
21 |
22 | ## Sample App Screenshot
23 |
24 | This repository also contains an example application so you can see it in action.
25 |
26 | You can see below some basic play controls, as well as a text view that prints out the byte ranges that have been successfully written to the current scratch file.
27 |
28 | Delete and reinstall the app to clear out the scratch file (or change the hard-coded MP3 URL to some other MP3 url and rebuild and run).
29 |
30 |
31 |
32 | ## Acknowledgements
33 |
34 | Contains a modified copy of [CommonCryptoSwift](https://github.com/onmyway133/Arcane), a convenient wrapper around CommonCrypto that can be used in a Swift framework.
35 |
--------------------------------------------------------------------------------
/Sodes/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaredsinclair/sodes-audio-example/72548e948d767ba0b3c2894c13b664c843fbd9a6/Sodes/.DS_Store
--------------------------------------------------------------------------------
/Sodes/Sodes.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Sodes/Sodes.xcodeproj/project.xcworkspace/xcshareddata/Cast.xcscmblueprint:
--------------------------------------------------------------------------------
1 | {
2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "0E1AB7D483F5968C839C9A65A2CE488A417D1236",
3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : {
4 |
5 | },
6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : {
7 | "EF7D7BF5F790B25DDE8A53D0271F72A88639FF4D" : 0,
8 | "0E1AB7D483F5968C839C9A65A2CE488A417D1236" : 0
9 | },
10 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "CB11FFCE-794D-4414-98CB-06B53A512BCD",
11 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : {
12 | "EF7D7BF5F790B25DDE8A53D0271F72A88639FF4D" : "cast\/Dependencies\/CommonCrypto.swift\/",
13 | "0E1AB7D483F5968C839C9A65A2CE488A417D1236" : "cast\/"
14 | },
15 | "DVTSourceControlWorkspaceBlueprintNameKey" : "Cast",
16 | "DVTSourceControlWorkspaceBlueprintVersion" : 204,
17 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "Cast\/Cast.xcodeproj",
18 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [
19 | {
20 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:jaredsinclair\/cast.git",
21 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
22 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "0E1AB7D483F5968C839C9A65A2CE488A417D1236"
23 | },
24 | {
25 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:jaredsinclair\/CommonCrypto.swift.git",
26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
27 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "EF7D7BF5F790B25DDE8A53D0271F72A88639FF4D"
28 | }
29 | ]
30 | }
--------------------------------------------------------------------------------
/Sodes/Sodes.xcodeproj/project.xcworkspace/xcshareddata/Sodes.xcscmblueprint:
--------------------------------------------------------------------------------
1 | {
2 | "DVTSourceControlWorkspaceBlueprintPrimaryRemoteRepositoryKey" : "F2432A7C07C18E9E3B0A9ABC8FB552B3106DF9F9",
3 | "DVTSourceControlWorkspaceBlueprintWorkingCopyRepositoryLocationsKey" : {
4 |
5 | },
6 | "DVTSourceControlWorkspaceBlueprintWorkingCopyStatesKey" : {
7 | "EF7D7BF5F790B25DDE8A53D0271F72A88639FF4D" : 0,
8 | "151CF6AE7A27E25C386CB7C06129C46E18BDA023" : 0,
9 | "EB2210CFD48672E403BED699D5D7F01B844069CF" : 9223372036854775807,
10 | "0E1AB7D483F5968C839C9A65A2CE488A417D1236" : 0,
11 | "F2432A7C07C18E9E3B0A9ABC8FB552B3106DF9F9" : 9223372036854775807
12 | },
13 | "DVTSourceControlWorkspaceBlueprintIdentifierKey" : "E9EE63F7-A4C5-4163-BAA4-8C55549B40B3",
14 | "DVTSourceControlWorkspaceBlueprintWorkingCopyPathsKey" : {
15 | "EF7D7BF5F790B25DDE8A53D0271F72A88639FF4D" : "sodes-sample\/Dependencies\/CommonCrypto.swift\/",
16 | "151CF6AE7A27E25C386CB7C06129C46E18BDA023" : "sodes-sample\/Dependencies\/JTSReachability\/",
17 | "EB2210CFD48672E403BED699D5D7F01B844069CF" : "sodes-sample\/Dependencies\/SWXMLHash\/",
18 | "0E1AB7D483F5968C839C9A65A2CE488A417D1236" : "sodes-sample\/",
19 | "F2432A7C07C18E9E3B0A9ABC8FB552B3106DF9F9" : "sodes-audio-example\/"
20 | },
21 | "DVTSourceControlWorkspaceBlueprintNameKey" : "Sodes",
22 | "DVTSourceControlWorkspaceBlueprintVersion" : 204,
23 | "DVTSourceControlWorkspaceBlueprintRelativePathToProjectKey" : "Sodes\/Sodes.xcodeproj",
24 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoriesKey" : [
25 | {
26 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:jaredsinclair\/cast.git",
27 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
28 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "0E1AB7D483F5968C839C9A65A2CE488A417D1236"
29 | },
30 | {
31 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:jaredsinclair\/JTSReachability.git",
32 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
33 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "151CF6AE7A27E25C386CB7C06129C46E18BDA023"
34 | },
35 | {
36 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:jaredsinclair\/SWXMLHash.git",
37 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
38 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "EB2210CFD48672E403BED699D5D7F01B844069CF"
39 | },
40 | {
41 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:jaredsinclair\/CommonCrypto.swift.git",
42 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
43 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "EF7D7BF5F790B25DDE8A53D0271F72A88639FF4D"
44 | },
45 | {
46 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryURLKey" : "github.com:jaredsinclair\/sodes-audio-example.git",
47 | "DVTSourceControlWorkspaceBlueprintRemoteRepositorySystemKey" : "com.apple.dt.Xcode.sourcecontrol.Git",
48 | "DVTSourceControlWorkspaceBlueprintRemoteRepositoryIdentifierKey" : "F2432A7C07C18E9E3B0A9ABC8FB552B3106DF9F9"
49 | }
50 | ]
51 | }
--------------------------------------------------------------------------------
/Sodes/Sodes.xcodeproj/xcshareddata/xcschemes/SodesAudio.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
65 |
66 |
67 |
68 |
78 |
79 |
85 |
86 |
87 |
88 |
89 |
90 |
96 |
97 |
103 |
104 |
105 |
106 |
108 |
109 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/Sodes/Sodes.xcodeproj/xcshareddata/xcschemes/SodesFoundation.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
44 |
45 |
47 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
65 |
66 |
67 |
68 |
78 |
79 |
85 |
86 |
87 |
88 |
89 |
90 |
96 |
97 |
103 |
104 |
105 |
106 |
108 |
109 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/Sodes/Sodes.xcodeproj/xcshareddata/xcschemes/SwiftableFileHandle.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
34 |
35 |
45 |
46 |
52 |
53 |
54 |
55 |
56 |
57 |
63 |
64 |
70 |
71 |
72 |
73 |
75 |
76 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/Sodes/SodesAudio/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaredsinclair/sodes-audio-example/72548e948d767ba0b3c2894c13b664c843fbd9a6/Sodes/SodesAudio/.DS_Store
--------------------------------------------------------------------------------
/Sodes/SodesAudio/AVContentInfoRequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AVContentInfoRequest.swift
3 | // SodesAudio
4 | //
5 | // Created by Jared Sinclair on 7/24/16.
6 | //
7 | //
8 |
9 | import Foundation
10 | import AVFoundation
11 | import MobileCoreServices
12 |
13 | internal extension AVAssetResourceLoadingContentInformationRequest {
14 |
15 | func update(with response: URLResponse) {
16 |
17 | if let response = response as? HTTPURLResponse {
18 |
19 | // TODO: [MEDIUM] Obtain the actual content type.
20 | contentType = "public.mp3"
21 |
22 | if let length = response.sodes_expectedContentLength {
23 | contentLength = length
24 | }
25 |
26 | if let acceptRanges = response.allHeaderFields["Accept-Ranges"] as? String,
27 | acceptRanges == "bytes"
28 | {
29 | isByteRangeAccessSupported = true
30 | } else {
31 | isByteRangeAccessSupported = false
32 | }
33 | }
34 | else {
35 | assertionFailure("Invalid URL Response.")
36 | }
37 | }
38 |
39 | }
40 |
41 |
42 |
--------------------------------------------------------------------------------
/Sodes/SodesAudio/ByteRange.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ByteRange.swift
3 | // SodesAudio
4 | //
5 | // Created by Jared Sinclair on 7/24/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | protocol Summable: Comparable {
12 | static func +(lhs: Self, rhs: Self) -> Self
13 | static func -(lhs: Self, rhs: Self) -> Self
14 | func decremented() -> Self
15 | func toInt() -> Int
16 | }
17 |
18 | extension Int64: Summable {
19 | func decremented() -> Int64 {
20 | return self - 1
21 | }
22 | func toInt() -> Int {
23 | return Int(self)
24 | }
25 | }
26 |
27 | public typealias ByteRange = Range
28 |
29 | enum ByteRangeIndexPosition {
30 | case before
31 | case inside
32 | case after
33 | }
34 |
35 | extension Range where Bound: Summable {
36 |
37 | var length: Bound {
38 | return upperBound - lowerBound
39 | }
40 |
41 | var lastValidIndex: Bound {
42 | return upperBound.decremented()
43 | }
44 |
45 | var subdataRange: Range {
46 | return lowerBound.toInt()..<(upperBound.toInt())
47 | }
48 |
49 | func leadingIntersection(in otherRange: Range) -> Range? {
50 | if lowerBound <= otherRange.lowerBound && lastValidIndex >= otherRange.lowerBound {
51 | if lastValidIndex > otherRange.lastValidIndex {
52 | return otherRange
53 | } else {
54 | let lowerBound = otherRange.lowerBound
55 | let upperBound = otherRange.lowerBound + length - otherRange.lowerBound
56 | return (lowerBound.. Range? {
64 | if let leading = leadingIntersection(in: otherRange), !fullySatisfies(otherRange) {
65 | return ((otherRange.lowerBound + leading.length).. Bool {
72 | if let intersection = leadingIntersection(in: requestedRange) {
73 | return intersection == requestedRange
74 | } else {
75 | return false
76 | }
77 | }
78 |
79 | func intersects(_ otherRange: Range) -> Bool {
80 | return otherRange.lowerBound < upperBound && lowerBound < otherRange.upperBound
81 | }
82 |
83 | func isContiguousWith(_ otherRange: Range) -> Bool {
84 | if otherRange.upperBound == lowerBound {
85 | return true
86 | } else if upperBound == otherRange.lowerBound {
87 | return true
88 | } else {
89 | return false
90 | }
91 | }
92 |
93 | func relativePosition(of index: Bound) -> ByteRangeIndexPosition {
94 | if index < lowerBound {
95 | return .before
96 | } else if index >= upperBound {
97 | return .after
98 | } else {
99 | return .inside
100 | }
101 | }
102 |
103 | }
104 |
105 | func combine(_ ranges: [ByteRange]) -> [ByteRange] {
106 | var combinedRanges = [ByteRange]()
107 | let uncheckedRanges = ranges.sorted{$0.length > $1.length}
108 | for uncheckedRange in uncheckedRanges {
109 | let intersectingRanges = combinedRanges.filter{
110 | $0.intersects(uncheckedRange) || $0.isContiguousWith(uncheckedRange)
111 | }
112 | if intersectingRanges.isEmpty {
113 | combinedRanges.append(uncheckedRange)
114 | } else {
115 | for range in intersectingRanges {
116 | if let index = combinedRanges.index(of: range) {
117 | combinedRanges.remove(at: index)
118 | }
119 | }
120 | let combinedRange = intersectingRanges.reduce(uncheckedRange, +)
121 | combinedRanges.append(combinedRange)
122 | }
123 | }
124 | return combinedRanges.sorted{$0.lowerBound < $1.lowerBound}
125 | }
126 |
127 | /// Adding byte ranges is currently very naive. It takes the lowest lowerBound
128 | /// and the highest upper bound and computes a range between the two. It assumes
129 | /// that the programmer desires this behavior, for instance, when you're adding
130 | /// a sequence of byte ranges which form a continuous range when summed as a
131 | /// whole even though any two random members might not overlap or be contiguous.
132 | private func +(lhs: ByteRange, rhs: ByteRange) -> ByteRange {
133 | let lowerBound = min(lhs.lowerBound, rhs.lowerBound)
134 | let upperBound = max(lhs.upperBound, rhs.upperBound)
135 | return (lowerBound.. Data? {
18 | if Int64(count) >= range.length {
19 | return subdata(in: (0.. [Operation] {
114 | return subrequests.map { (subrequest) -> Operation in
115 | switch subrequest.source {
116 | case .scratchFile:
117 | return newScratchFileOperation(for: subrequest.range)
118 | case .network:
119 | return newNetworkRequestOperation(for: resourceUrl, range: subrequest.range)
120 | }
121 | }
122 | }
123 |
124 | /// Creates an operation which will load `range` from the scratch file.
125 | fileprivate func newScratchFileOperation(for range: ByteRange) -> Operation {
126 | SodesLog("Creating operation for scratch file for range: \(range)")
127 | return BlockOperation { [weak self] in
128 | guard let this = self else {return}
129 | guard !this.cancelled && !this.failed else {return}
130 | do {
131 | let data = try this.scratchFileHandle.read(from: range)
132 | this.currentOffset = range.upperBound
133 | this.callbackQueue.sync { [weak this] in
134 | guard let this = this else {return}
135 | guard !this.cancelled && !this.failed else {return}
136 | SodesLog("Sucessfully read data from the scratch file: \(range)")
137 | this.delegate?.dataRequestLoader(this, didReceive: data, forSubrange: range)
138 | }
139 | } catch (let e) {
140 | SodesLog(e)
141 | this.fail(with: e)
142 | }
143 | }
144 | }
145 |
146 | /// Creates an operation which will download `range` from `url`.
147 | fileprivate func newNetworkRequestOperation(for url: URL, range: ByteRange) -> Operation {
148 | SodesLog("Creating operation for byte range: \(range).")
149 | return DelegatedHTTPOperation(
150 | request: URLRequest.dataRequest(from: url, for: range),
151 | configuration: .default,
152 | dataDelegate: self,
153 | delegateQueue: httpCallbackQueue,
154 | completion: { [weak self] (result) in
155 | guard let this = self else {return}
156 | guard !this.cancelled && !this.failed else {return}
157 | switch result {
158 | case .success(_, let bytesExpected, let bytesReceived):
159 | SodesLog("bytesExpected: \(bytesExpected), bytesReceived: \(bytesReceived)")
160 | // No-op, just let the procedure continue onto the next operation
161 | break
162 | case .error(let response,let error):
163 | SodesLog("\(error): \(response)")
164 | this.fail(with: error)
165 | }
166 | })
167 | }
168 |
169 | /// Safely moves the request loader into its failed state.
170 | fileprivate func fail(with error: Error?) {
171 | failed = true
172 | operationQueue.cancelAllOperations()
173 | operationQueue.addOperation { [weak self] in
174 | guard let this = self else {return}
175 | guard !this.cancelled else {return}
176 | this.callbackQueue.async {
177 | this.delegate?.dataRequestLoader(this, didFailWithError: error)
178 | }
179 | }
180 | }
181 |
182 | }
183 |
184 | extension DataRequestLoader: HTTPOperationDataDelegate {
185 |
186 | /// Will be called when the operation receives a success-level response.
187 | /// This gives the receiver a chance to respond to it in case there's a
188 | /// domain-specific error.
189 | func delegatedHTTPOperation(_ operation: DelegatedHTTPOperation, didReceiveResponse response: HTTPURLResponse) {
190 |
191 | assert(OperationQueue.current === httpCallbackQueue)
192 |
193 | // As of this writing, DelegatedHTTPOperation should treat status codes
194 | // outside this 200-level range as errors, not calling this delegate
195 | // method but finishing with an error.
196 | assert(200 <= response.statusCode && response.statusCode <= 299)
197 |
198 | if !response.sodes_isByteRangeEnabledResponse {
199 | assertionFailure("Byte ranges not supported by this server. What gives?")
200 | fail(with: SodesAudioError.byteRangeAccessNotSupported(response))
201 | }
202 |
203 | }
204 |
205 | /// This will be called many times a second as chunks of data are loaded.
206 | /// The receiver will write the data to the scratch file and, if successful,
207 | /// will notify its delegate of the loaded data. The receiver will pass the
208 | /// data onto AVFoundation and update the metadata registry of loaded
209 | /// byte ranges.
210 | func delegatedHTTPOperation(_ operation: DelegatedHTTPOperation, didReceiveData data: Data) {
211 |
212 | assert(OperationQueue.current === httpCallbackQueue)
213 |
214 | do {
215 | try scratchFileHandle.write(data, at: UInt64(currentOffset))
216 | scratchFileHandle.synchronizeFile()
217 | let range: ByteRange = (currentOffset..
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 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Sodes/SodesAudio/MiscExtensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MiscExtensions.swift
3 | // SodesAudio
4 | //
5 | // Created by Jared Sinclair on 7/16/16.
6 | //
7 | //
8 |
9 | import Foundation
10 | import AVFoundation
11 |
12 | internal let preferredTimescale: Int32 = 10000
13 |
14 | internal extension UInt {
15 | static func withInterval(_ i: TimeInterval?) -> UInt? {
16 | if let i = i {
17 | return UInt(i)
18 | } else {
19 | return nil
20 | }
21 | }
22 | }
23 |
24 | internal extension TimeInterval {
25 | var asCMTime: CMTime {
26 | return CMTimeMakeWithSeconds(self, preferredTimescale)
27 | }
28 | }
29 |
30 | internal extension CMTime {
31 | var asTimeInterval: TimeInterval? {
32 | if isNumeric {
33 | return CMTimeGetSeconds(self)
34 | } else {
35 | return nil
36 | }
37 | }
38 | }
39 |
40 | internal extension AVPlayer {
41 |
42 | var isPlaying: Bool {
43 | return error == nil && rate != 0 && currentItem != nil
44 | }
45 |
46 | var isPaused: Bool {
47 | return error == nil && rate == 0 && currentItem != nil
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/Sodes/SodesAudio/NSObject.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSObject.swift
3 | // SodesAudio
4 | //
5 | // Created by Jared Sinclair on 7/10/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | internal extension NSObject {
12 |
13 | func add(observer: NSObject, for keypaths: [String], options: NSKeyValueObservingOptions = .new, context: UnsafeMutableRawPointer) {
14 | for path in keypaths {
15 | addObserver(observer, forKeyPath: path, options: options, context: context)
16 | }
17 | }
18 |
19 | func remove(observer: NSObject, for keypaths: [String], options: NSKeyValueObservingOptions = .new, context: UnsafeMutableRawPointer) {
20 | for path in keypaths {
21 | removeObserver(observer, forKeyPath: path, context: context)
22 | }
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/Sodes/SodesAudio/PlaybackSource.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PlaybackSource.swift
3 | // SodesAudio
4 | //
5 | // Created by Jared Sinclair on 7/16/16.
6 | //
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import MediaPlayer
12 | import SodesFoundation
13 |
14 | public protocol PlaybackSource {
15 | var uniqueId: String {get}
16 | var artistId: String {get}
17 | var remoteUrl: URL {get}
18 | var title: String? {get}
19 | var albumTitle: String? {get}
20 | var artist: String? {get}
21 | var artworkUrl: URL? {get}
22 | var mediaType: MPMediaType {get}
23 | var expectedLengthInBytes: Int64? {get}
24 | }
25 |
26 | internal extension PlaybackSource {
27 |
28 | func nowPlayingInfo(image: UIImage? = nil, duration: TimeInterval? = nil, elapsedPlaybackTime: TimeInterval? = nil, rate: Double? = nil) -> [String: AnyObject] {
29 |
30 | var info: [String: AnyObject] = [:]
31 |
32 | info[MPMediaItemPropertyMediaType] = NSNumber(value: mediaType.rawValue)
33 |
34 | if let title = title {
35 | info[MPMediaItemPropertyTitle] = title as NSString
36 | }
37 | if let albumTitle = albumTitle {
38 | info[MPMediaItemPropertyAlbumTitle] = albumTitle as NSString
39 | }
40 | if let artist = artist {
41 | info[MPMediaItemPropertyArtist] = artist as NSString
42 | info[MPMediaItemPropertyAlbumArtist] = artist as NSString
43 | }
44 | if let image = image {
45 | let artwork = MPMediaItemArtwork(boundsSize: image.size) { (inputSize) -> UIImage in
46 | return image.draw(at: inputSize)
47 | }
48 | info[MPMediaItemPropertyArtwork] = artwork
49 | }
50 | if let duration = UInt.withInterval(duration) {
51 | info[MPMediaItemPropertyPlaybackDuration] = NSNumber(value: duration)
52 | }
53 | if let time = elapsedPlaybackTime {
54 | info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = NSNumber(value: time)
55 | }
56 | if let rate = rate {
57 | info[MPNowPlayingInfoPropertyPlaybackRate] = NSNumber(value: rate)
58 | }
59 |
60 | return info
61 | }
62 |
63 | }
64 |
65 | private extension UIImage {
66 |
67 | func draw(at targetSize: CGSize) -> UIImage {
68 |
69 | guard !self.size.equalTo(CGSize.zero) else {
70 | SodesLog("Invalid image size: (0,0)")
71 | return self
72 | }
73 |
74 | guard !targetSize.equalTo(CGSize.zero) else {
75 | SodesLog("Invalid target size: (0,0)")
76 | return self
77 | }
78 |
79 | let scaledSize = sizeThatFills(targetSize)
80 | let x = (targetSize.width - scaledSize.width) / 2.0
81 | let y = (targetSize.height - scaledSize.height) / 2.0
82 | let drawingRect = CGRect(x: x, y: y, width: scaledSize.width, height: scaledSize.height)
83 |
84 | UIGraphicsBeginImageContextWithOptions(targetSize, true, 0)
85 | draw(in: drawingRect)
86 | let resizedImage = UIGraphicsGetImageFromCurrentImageContext()
87 | UIGraphicsEndImageContext()
88 |
89 | return resizedImage!
90 |
91 | }
92 |
93 | func sizeThatFills(_ other: CGSize) -> CGSize {
94 | guard !size.equalTo(CGSize.zero) else {
95 | return other
96 | }
97 | let heightRatio = other.height / size.height
98 | let widthRatio = other.width / size.width
99 | if heightRatio > widthRatio {
100 | return CGSize(width: size.width * heightRatio, height: size.height * heightRatio)
101 | } else {
102 | return CGSize(width: size.width * widthRatio, height: size.height * widthRatio)
103 | }
104 | }
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/Sodes/SodesAudio/Requests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Requests.swift
3 | // SodesAudio
4 | //
5 | // Created by Jared Sinclair on 8/9/16.
6 | //
7 | //
8 |
9 | import Foundation
10 | import AVFoundation
11 |
12 | protocol Request: class {
13 | var resourceUrl: URL {get}
14 | var loadingRequest: AVAssetResourceLoadingRequest {get}
15 |
16 | func cancel()
17 | }
18 |
19 | class ContentInfoRequest: Request {
20 |
21 | let resourceUrl: URL
22 | let loadingRequest: AVAssetResourceLoadingRequest
23 | let infoRequest: AVAssetResourceLoadingContentInformationRequest
24 | let task: URLSessionTask
25 |
26 | init(resourceUrl: URL, loadingRequest: AVAssetResourceLoadingRequest, infoRequest: AVAssetResourceLoadingContentInformationRequest, task: URLSessionTask) {
27 | self.resourceUrl = resourceUrl
28 | self.loadingRequest = loadingRequest
29 | self.infoRequest = infoRequest
30 | self.task = task
31 | }
32 |
33 | func cancel() {
34 | task.cancel()
35 | if !loadingRequest.isCancelled && !loadingRequest.isFinished {
36 | loadingRequest.finishLoading()
37 | }
38 | }
39 |
40 | }
41 |
42 | class DataRequest: Request {
43 |
44 | let resourceUrl: URL
45 | let loadingRequest: AVAssetResourceLoadingRequest
46 | let dataRequest: AVAssetResourceLoadingDataRequest
47 | let loader: DataRequestLoader
48 |
49 | init(resourceUrl: URL, loadingRequest: AVAssetResourceLoadingRequest, dataRequest: AVAssetResourceLoadingDataRequest, loader: DataRequestLoader) {
50 | self.resourceUrl = resourceUrl
51 | self.loadingRequest = loadingRequest
52 | self.dataRequest = dataRequest
53 | self.loader = loader
54 | }
55 |
56 | func cancel() {
57 | loader.cancel()
58 | if !loadingRequest.isCancelled && !loadingRequest.isFinished {
59 | loadingRequest.finishLoading()
60 | }
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/Sodes/SodesAudio/ResourceLoaderSubrequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResourceLoaderSubrequest.swift
3 | // SodesAudio
4 | //
5 | // Created by Jared Sinclair on 8/5/16.
6 | //
7 | //
8 |
9 | import Foundation
10 | import SodesFoundation
11 |
12 | /// An intermediate struct useful for calculating which subranges of data can be
13 | /// read from a scratch file versus downloaded from the Internet.
14 | struct ResourceLoaderSubrequest: Equatable, CustomStringConvertible {
15 |
16 | /// The sources from which data can be read.
17 | enum Source {
18 | case scratchFile
19 | case network
20 | }
21 |
22 | /// The source from which the data must be read.
23 | let source: Source
24 |
25 | /// The subrequest range (only a portion of the entire requested range).
26 | let range: ByteRange
27 |
28 | /// Description for debugging.
29 | var description: String {
30 | var s = "ResourceLoaderSubrequest("
31 | switch source {
32 | case .scratchFile:
33 | s += ".scratchFile"
34 | case .network:
35 | s += ".network"
36 | }
37 | s += ", "
38 | s += range.description
39 | s += ")"
40 | return s
41 | }
42 | }
43 |
44 | func ==(lhs: ResourceLoaderSubrequest, rhs: ResourceLoaderSubrequest) -> Bool {
45 | return lhs.source == rhs.source && lhs.range == rhs.range
46 | }
47 |
48 | extension ResourceLoaderSubrequest {
49 |
50 | /// Creates an array of subrequests. It will create scratch file subrequests
51 | /// for data already loaded in `scratchFileRanges`, and network subrequests
52 | /// for any gaps in the requested range not found in `scratchFileRanges`.
53 | static func subrequests(requestedRange: ByteRange, scratchFileRanges: [ByteRange]) -> [ResourceLoaderSubrequest] {
54 |
55 | var subrequests: [ResourceLoaderSubrequest] = []
56 |
57 | let intersectingRanges = scratchFileRanges
58 | .filter{$0.intersects(requestedRange)}
59 | .sorted{$0.lowerBound < $1.lowerBound}
60 |
61 | if intersectingRanges.isEmpty {
62 | let networkSubrequest = ResourceLoaderSubrequest(source: .network, range: requestedRange)
63 | subrequests.append(networkSubrequest)
64 | return subrequests
65 | }
66 |
67 | var cursor = requestedRange.lowerBound
68 |
69 | var nextRangeIndex = 0
70 |
71 | while cursor < requestedRange.upperBound && nextRangeIndex < intersectingRanges.count {
72 |
73 | let nextRange = intersectingRanges[nextRangeIndex]
74 | let position = nextRange.relativePosition(of: cursor)
75 |
76 | switch position {
77 | case .before:
78 | let lowerBound = cursor
79 | let upperBound = min(nextRange.lowerBound, requestedRange.upperBound)
80 | let networkRequest = ResourceLoaderSubrequest(
81 | source: .network,
82 | range: (lowerBound.. Data {
16 | do {
17 | let data = try readData(
18 | fromLocation: UInt64(range.lowerBound),
19 | length: UInt64(range.length)
20 | )
21 | return data
22 | } catch {
23 | throw(error)
24 | }
25 | }
26 |
27 | func write(_ data: Data, over range: ByteRange) throws {
28 | do {
29 | try write(data, at: UInt64(range.lowerBound))
30 | } catch {
31 | throw(error)
32 | }
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/Sodes/SodesAudio/ScratchFileInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScratchFileInfo.swift
3 | // SodesAudio
4 | //
5 | // Created by Jared Sinclair on 8/16/16.
6 | //
7 | //
8 |
9 | import Foundation
10 | import SwiftableFileHandle
11 | import SodesFoundation
12 |
13 | /// Info for the current scratch file for ResourceLoaderDelegate.
14 | class ScratchFileInfo {
15 |
16 | /// Cache validation info.
17 | struct CacheInfo {
18 | let contentLength: Int64?
19 | let etag: String?
20 | let lastModified: Date?
21 | }
22 |
23 | /// The remote URL from which the file should be downloaded.
24 | let resourceUrl: URL
25 |
26 | /// The subdirectory for this scratch file and its metadata.
27 | let directory: URL
28 |
29 | /// The file url to the scratch file itself.
30 | let scratchFileUrl: URL
31 |
32 | /// The file url to the metadata for the scratch file.
33 | let metaDataUrl: URL
34 |
35 | /// The file handle used for reading/writing bytes to the scratch file.
36 | let fileHandle: SODSwiftableFileHandle
37 |
38 | /// The most recent cache validation info.
39 | var cacheInfo: CacheInfo
40 |
41 | /// The byte ranges for the scratch file that have been saved thus far.
42 | var loadedByteRanges: [ByteRange]
43 |
44 | /// Designated initializer.
45 | init(resourceUrl: URL, directory: URL, scratchFileUrl: URL, metaDataUrl: URL, fileHandle: SODSwiftableFileHandle, cacheInfo: CacheInfo, loadedByteRanges: [ByteRange]) {
46 | self.resourceUrl = resourceUrl
47 | self.directory = directory
48 | self.scratchFileUrl = scratchFileUrl
49 | self.metaDataUrl = metaDataUrl
50 | self.fileHandle = fileHandle
51 | self.cacheInfo = cacheInfo
52 | self.loadedByteRanges = loadedByteRanges
53 | }
54 |
55 | }
56 |
57 | extension ScratchFileInfo.CacheInfo {
58 |
59 | static let none = ScratchFileInfo.CacheInfo(
60 | contentLength: nil, etag: nil, lastModified: nil
61 | )
62 |
63 | func isStillValid(comparedTo otherInfo: ScratchFileInfo.CacheInfo) -> Bool {
64 | if let old = self.etag, let new = otherInfo.etag {
65 | return old == new
66 | }
67 | else if let old = lastModified, let new = otherInfo.lastModified {
68 | return old == new
69 | }
70 | else {
71 | return false
72 | }
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/Sodes/SodesAudio/ScratchFileInfoSerialization.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ScratchFileInfoSerialization.swift
3 | // SodesAudio
4 | //
5 | // Created by Jared Sinclair on 8/5/16.
6 | //
7 | //
8 |
9 | import Foundation
10 | import SodesFoundation
11 |
12 | extension PropertyListSerialization {
13 |
14 | /// Converts the input values into a data object that can be serialized as a
15 | /// property list.
16 | static func representation(for byteRanges: [ByteRange], cacheInfo: ScratchFileInfo.CacheInfo) -> Data? {
17 | var plist: [String: AnyObject] = [
18 | "byteRanges": byteRanges.map{stringRepresentation(for: $0)} as NSArray
19 | ]
20 | if let length = cacheInfo.contentLength {
21 | plist["contentLength"] = NSNumber(value: length)
22 | }
23 | if let etag = cacheInfo.etag {
24 | plist["etag"] = etag as NSString
25 | }
26 | if let lastModified = cacheInfo.lastModified {
27 | plist["lastModified"] = lastModified as NSDate
28 | }
29 | return try? data(fromPropertyList: plist, format: .xml, options: 0)
30 | }
31 |
32 | /// Converts property list data into byte ranges and cache info.
33 | static func byteRangesAndCacheInfo(from representation: Data) -> ([ByteRange], ScratchFileInfo.CacheInfo)? {
34 | guard
35 | let value = try? propertyList(from: representation, format: nil), let plist = value as? [String: Any],
36 | let strings = plist["byteRanges"] as? [String]
37 | else
38 | {
39 | return nil
40 | }
41 | let byteRanges = strings.flatMap{range(from: $0)}
42 | let info = ScratchFileInfo.CacheInfo(
43 | contentLength: (plist["contentLength"] as? NSNumber)?.int64Value,
44 | etag: plist["etag"] as? String,
45 | lastModified: plist["lastModified"] as? Date
46 | )
47 | return (byteRanges, info)
48 | }
49 |
50 | /// Convenience method.
51 | private static func stringRepresentation(for range: ByteRange) -> String {
52 | return "\(range.lowerBound)..<\(range.upperBound)"
53 | }
54 |
55 | /// Convenience method.
56 | private static func range(from string: String) -> ByteRange? {
57 | let comps = string.components(separatedBy: "..<")
58 | if comps.count == 2 {
59 | if let l = Int64(comps[0]), let u = Int64(comps[1]) {
60 | return (l.. Bool {
72 | if let data = PropertyListSerialization.representation(for: byteRanges, cacheInfo: cacheInfo) {
73 | do {
74 | try data.write(to: fileUrl, options: .atomic)
75 | return true
76 | } catch {
77 | SodesLog(error)
78 | return false
79 | }
80 | } else {
81 | return false
82 | }
83 | }
84 |
85 | /// Reads the values from a property list found at `fileUrl`.
86 | func readRanges(at fileUrl: URL) -> ([ByteRange], ScratchFileInfo.CacheInfo)? {
87 | if let data = try? Data(contentsOf: fileUrl) {
88 | return PropertyListSerialization.byteRangesAndCacheInfo(from: data)
89 | } else {
90 | return nil
91 | }
92 | }
93 |
94 | }
95 |
--------------------------------------------------------------------------------
/Sodes/SodesAudio/URL.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URL.swift
3 | // SodesAudio
4 | //
5 | // Created by Jared Sinclair on 7/24/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | internal extension URL {
12 |
13 | /// Returns true if the receiver's path extension is equal to `pathExt`.
14 | func hasPathExtension(_ pathExt: String) -> Bool {
15 | guard let comps = URLComponents(url: self, resolvingAgainstBaseURL: false) else {return false}
16 | return (comps.path as NSString).pathExtension == pathExt
17 | }
18 |
19 | /// Adds the scheme prefix to a copy of the receiver.
20 | func convertToRedirectURL(prefix: String) -> URL? {
21 | guard var comps = URLComponents(url: self, resolvingAgainstBaseURL: false) else {return nil}
22 | guard let scheme = comps.scheme else {return nil}
23 | comps.scheme = prefix + scheme
24 | return comps.url
25 | }
26 |
27 | /// Removes the scheme prefix from a copy of the receiver.
28 | func convertFromRedirectURL(prefix: String) -> URL? {
29 | guard var comps = URLComponents(url: self, resolvingAgainstBaseURL: false) else {return nil}
30 | guard let scheme = comps.scheme else {return nil}
31 | comps.scheme = scheme.replacingOccurrences(of: prefix, with: "")
32 | return comps.url
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/Sodes/SodesAudio/URLRequest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLRequest.swift
3 | // SodesAudio
4 | //
5 | // Created by Jared Sinclair on 8/19/16.
6 | //
7 | //
8 |
9 | import Foundation
10 | import AVFoundation
11 |
12 | extension AVAssetResourceLoadingDataRequest {
13 |
14 | var byteRange: ByteRange {
15 | let lowerBound = requestedOffset
16 | let upperBound = (lowerBound + requestedLength - 1)
17 | return (lowerBound.. URLRequest {
47 | var request = URLRequest(url: url)
48 | request.setByteRangeHeader(for: range)
49 | request.cachePolicy = .reloadIgnoringLocalCacheData
50 | return request
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/Sodes/SodesAudio/URLResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLResponse.swift
3 | // SodesAudio
4 | //
5 | // Created by Jared Sinclair on 8/20/16.
6 | //
7 | //
8 |
9 | import Foundation
10 | import SodesFoundation
11 |
12 | extension URLResponse {
13 |
14 | func cacheInfo(using formatter: RFC822Formatter) -> ScratchFileInfo.CacheInfo {
15 | return ScratchFileInfo.CacheInfo(
16 | contentLength: sodes_expectedContentLength,
17 | etag: etag,
18 | lastModified: lastModified(using: formatter)
19 | )
20 | }
21 |
22 | var sodes_isByteRangeEnabledResponse: Bool {
23 | return sodes_responseRange != nil
24 | }
25 |
26 | var sodes_responseRange: ByteRange? {
27 | guard let response = self as? HTTPURLResponse else {return nil}
28 | if let fullString = response.allHeaderFields["Content-Range"] as? String,
29 | let firstPart = fullString.characters.split(separator: "/").map({String($0)}).first
30 | {
31 | if let prefixRange = firstPart.range(of: "bytes ") {
32 | let rangeString = firstPart.substring(from: prefixRange.upperBound)
33 | let comps = rangeString.components(separatedBy: "-")
34 | let ints = comps.flatMap{Int64($0)}
35 | if ints.count == 2 {
36 | return (ints[0]..<(ints[1]+1))
37 | }
38 | }
39 | }
40 | return nil
41 | }
42 |
43 | var sodes_expectedContentLength: Int64? {
44 | guard let response = self as? HTTPURLResponse else {return nil}
45 | if let rangeString = response.allHeaderFields["Content-Range"] as? String,
46 | let bytesString = rangeString.characters.split(separator: "/").map({String($0)}).last,
47 | let bytes = Int64(bytesString)
48 | {
49 | return bytes
50 | } else {
51 | return nil
52 | }
53 | }
54 |
55 | var etag: String? {
56 | guard let response = self as? HTTPURLResponse else {return nil}
57 | return response.allHeaderFields["Etag"] as? String
58 | }
59 |
60 | func lastModified(using formatter: RFC822Formatter) -> Date? {
61 | guard let response = self as? HTTPURLResponse else {return nil}
62 | if let string = response.allHeaderFields["Last-Modified"] as? String {
63 | return formatter.date(from: string)
64 | } else {
65 | return nil
66 | }
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/Sodes/SodesAudioTests/ByteRangeSerializationTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ByteRangeSerializationTests.swift
3 | // SodesAudioTests
4 | //
5 | // Created by Jared Sinclair on 8/5/16.
6 | //
7 | //
8 |
9 | import XCTest
10 | import SodesFoundation
11 | @testable import SodesAudio
12 |
13 | class ByteRangeSerializationTests: XCTestCase {
14 |
15 | // MARK: JSON Serialization
16 |
17 | func testItConvertsByteRangesToData() {
18 | XCTAssertNotNil(PropertyListSerialization.representation(for: [], cacheInfo: .none))
19 | XCTAssertNotNil(PropertyListSerialization.representation(for:[(0..<1)], cacheInfo: .none))
20 | XCTAssertNotNil(PropertyListSerialization.representation(for:[(0..<1), (1..<2), (2..<3)], cacheInfo: .none))
21 | }
22 |
23 | func testItConvertsByteRangesToDataAndBackAgain() {
24 |
25 | var input: [ByteRange]!
26 | var data: Data!
27 | var output: ([ByteRange], ScratchFileInfo.CacheInfo)!
28 |
29 | input = []
30 | data = PropertyListSerialization.representation(for:input, cacheInfo: .none)
31 | output = PropertyListSerialization.byteRangesAndCacheInfo(from: data)
32 | XCTAssertEqual(input, output.0)
33 |
34 | input = [(0..<1)]
35 | data = PropertyListSerialization.representation(for:input, cacheInfo: .none)
36 | output = PropertyListSerialization.byteRangesAndCacheInfo(from: data)
37 | XCTAssertEqual(input, output.0)
38 |
39 | input = [(0..<1), (1..<2), (2..<3)]
40 | data = PropertyListSerialization.representation(for:input, cacheInfo: .none)
41 | output = PropertyListSerialization.byteRangesAndCacheInfo(from: data)
42 | XCTAssertEqual(input, output.0)
43 |
44 | }
45 |
46 | // MARK: File Management
47 |
48 | func testItSavesByteRangesAndReadsThemBack() {
49 | let directory = FileManager.default.cachesDirectory()!
50 | let fileUrl = directory.appendingPathComponent(UUID().uuidString, isDirectory: false)
51 | let inputRanges: [ByteRange] = [(0..<1), (3..<10)]
52 | let inputDate = Date()
53 | let contentLength: Int64 = 2056
54 | let inputInfo = ScratchFileInfo.CacheInfo(
55 | contentLength: contentLength, etag: "e", lastModified: inputDate
56 | )
57 | XCTAssertTrue(FileManager.default.save(byteRanges: inputRanges, cacheInfo: inputInfo, to: fileUrl))
58 | let output = FileManager.default.readRanges(at: fileUrl)
59 | XCTAssertEqual(contentLength, output!.1.contentLength)
60 | XCTAssertEqual(inputRanges, output!.0)
61 | XCTAssertEqual("e", output!.1.etag)
62 | XCTAssertEqualWithAccuracy(inputDate.timeIntervalSince(output!.1.lastModified!), 0.0, accuracy: 1.0)
63 | }
64 |
65 | func testItSavesByteRangesTwiceAndReadsThemBack() {
66 |
67 | let directory = FileManager.default.cachesDirectory()!
68 | let fileUrl = directory.appendingPathComponent(NSUUID().uuidString, isDirectory: false)
69 | let inputRanges1: [ByteRange] = [(0..<1), (3..<10)]
70 | let inputRanges2: [ByteRange] = [(0..<1), (16..<20), (3..<10)]
71 | let contentLength: Int64 = 2056
72 | let inputDate = Date()
73 | let inputInfo = ScratchFileInfo.CacheInfo(
74 | contentLength: contentLength, etag: "e", lastModified: inputDate
75 | )
76 |
77 | XCTAssertTrue(FileManager.default.save(byteRanges: inputRanges1, cacheInfo: inputInfo, to: fileUrl))
78 | let output1 = FileManager.default.readRanges(at: fileUrl)
79 | XCTAssertEqual(contentLength, output1!.1.contentLength)
80 | XCTAssertEqual(inputRanges1, output1!.0)
81 | XCTAssertEqual("e", output1!.1.etag)
82 | XCTAssertEqualWithAccuracy(inputDate.timeIntervalSince(output1!.1.lastModified!), 0.0, accuracy: 1.0)
83 |
84 | XCTAssertTrue(FileManager.default.save(byteRanges: inputRanges2, cacheInfo: inputInfo, to: fileUrl))
85 | let output2 = FileManager.default.readRanges(at: fileUrl)
86 | XCTAssertEqual(contentLength, output2!.1.contentLength)
87 | XCTAssertEqual(inputRanges2, output2!.0)
88 | XCTAssertEqual("e", output2!.1.etag)
89 | XCTAssertEqualWithAccuracy(inputDate.timeIntervalSince(output2!.1.lastModified!), 0.0, accuracy: 1.0)
90 |
91 | }
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/Sodes/SodesAudioTests/ByteRangeTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ByteRangeTests.swift
3 | // SodesAudioTests
4 | //
5 | // Created by Jared Sinclair on 8/5/16.
6 | //
7 | //
8 |
9 | import XCTest
10 | @testable import SodesAudio
11 |
12 | class ByteRangeTests: XCTestCase {
13 |
14 | // MARK: General
15 |
16 | func testItCreatesAValidRange() {
17 | let range: ByteRange = (0..<10)
18 | XCTAssertEqual(range.lowerBound, 0)
19 | XCTAssertEqual(range.upperBound, 10)
20 | XCTAssertEqual(range.lastValidIndex, 9)
21 | XCTAssertEqual(range.subdataRange, Range((0..<10)))
22 | }
23 |
24 | // MARK: Leading
25 |
26 | func testItComputesCorrectLeadingIntersections_SameStarts_DifferingEnds() {
27 |
28 | let a1: ByteRange = (0..<5)
29 | let b1: ByteRange = (0..<5)
30 | let expected1: ByteRange? = (0..<5)
31 | XCTAssertEqual(a1.leadingIntersection(in: b1), expected1)
32 |
33 | let a2: ByteRange = (0..<3)
34 | let b2: ByteRange = (0..<5)
35 | let expected2: ByteRange? = (0..<3)
36 | XCTAssertEqual(a2.leadingIntersection(in: b2), expected2)
37 |
38 | let a3: ByteRange = (0..<10)
39 | let b3: ByteRange = (0..<5)
40 | let expected3: ByteRange? = (0..<5)
41 | XCTAssertEqual(a3.leadingIntersection(in: b3), expected3)
42 |
43 | let a4: ByteRange = (0..<5)
44 | let b4: ByteRange = (0..<10)
45 | let expected4: ByteRange? = (0..<5)
46 | XCTAssertEqual(a4.leadingIntersection(in: b4), expected4)
47 |
48 | }
49 |
50 | func testItComputesCorrectLeadingIntersections_DifferingStarts_SameEnds() {
51 |
52 | let a1: ByteRange = (5..<10)
53 | let b1: ByteRange = (0..<10)
54 | let expected1: ByteRange? = nil
55 | XCTAssertEqual(a1.leadingIntersection(in: b1), expected1)
56 |
57 | let a2: ByteRange = (0..<10)
58 | let b2: ByteRange = (5..<10)
59 | let expected2: ByteRange? = (5..<10)
60 | XCTAssertEqual(a2.leadingIntersection(in: b2), expected2)
61 |
62 | }
63 |
64 | func testItComputesCorrectLeadingIntersections_DifferingStarts_DifferingEnds() {
65 |
66 | let a1: ByteRange = (0..<20)
67 | let b1: ByteRange = (5..<10)
68 | let expected1: ByteRange? = (5..<10)
69 | XCTAssertEqual(a1.leadingIntersection(in: b1), expected1)
70 |
71 | let a2: ByteRange = (5..<10)
72 | let b2: ByteRange = (0..<20)
73 | let expected2: ByteRange? = nil
74 | XCTAssertEqual(a2.leadingIntersection(in: b2), expected2)
75 |
76 | let a3: ByteRange = (10..<20)
77 | let b3: ByteRange = (5..<10)
78 | let expected3: ByteRange? = nil
79 | XCTAssertEqual(a3.leadingIntersection(in: b3), expected3)
80 |
81 | let a4: ByteRange = (5..<10)
82 | let b4: ByteRange = (10..<20)
83 | let expected4: ByteRange? = nil
84 | XCTAssertEqual(a4.leadingIntersection(in: b4), expected4)
85 |
86 | }
87 |
88 | func testItComputesCorrectLeadingIntersections_OffByOnes() {
89 |
90 | let a1: ByteRange = (0..<2)
91 | let b1: ByteRange = (1..<2)
92 | let expected1: ByteRange? = (1..<2)
93 | XCTAssertEqual(a1.leadingIntersection(in: b1), expected1)
94 |
95 | let a2: ByteRange = (1..<2)
96 | let b2: ByteRange = (0..<2)
97 | let expected2: ByteRange? = nil
98 | XCTAssertEqual(a2.leadingIntersection(in: b2), expected2)
99 |
100 | let a3: ByteRange = (1..<2)
101 | let b3: ByteRange = (0..<1)
102 | let expected3: ByteRange? = nil
103 | XCTAssertEqual(a3.leadingIntersection(in: b3), expected3)
104 |
105 | let a4: ByteRange = (0..<1)
106 | let b4: ByteRange = (1..<2)
107 | let expected4: ByteRange? = nil
108 | XCTAssertEqual(a4.leadingIntersection(in: b4), expected4)
109 |
110 | }
111 |
112 | // MARK: Trailing
113 |
114 | func testItComputesCorrectTrailingRanges_SameStarts_DifferingEnds() {
115 |
116 | let a1: ByteRange = (0..<5)
117 | let b1: ByteRange = (0..<5)
118 | let expected1: ByteRange? = nil
119 | XCTAssertEqual(a1.trailingRange(in: b1), expected1)
120 |
121 | let a2: ByteRange = (0..<3)
122 | let b2: ByteRange = (0..<5)
123 | let expected2: ByteRange? = (3..<5)
124 | XCTAssertEqual(a2.trailingRange(in: b2), expected2)
125 |
126 | let a3: ByteRange = (0..<10)
127 | let b3: ByteRange = (0..<5)
128 | let expected3: ByteRange? = nil
129 | XCTAssertEqual(a3.trailingRange(in: b3), expected3)
130 |
131 | let a4: ByteRange = (0..<5)
132 | let b4: ByteRange = (0..<10)
133 | let expected4: ByteRange? = (5..<10)
134 | XCTAssertEqual(a4.trailingRange(in: b4), expected4)
135 |
136 | }
137 |
138 | func testItComputesCorrectTrailingRanges_DifferingStarts_SameEnds() {
139 |
140 | let a1: ByteRange = (5..<10)
141 | let b1: ByteRange = (0..<10)
142 | let expected1: ByteRange? = nil
143 | XCTAssertEqual(a1.trailingRange(in: b1), expected1)
144 |
145 | let a2: ByteRange = (0..<10)
146 | let b2: ByteRange = (5..<10)
147 | let expected2: ByteRange? = nil
148 | XCTAssertEqual(a2.trailingRange(in: b2), expected2)
149 |
150 | }
151 |
152 | func testItComputesCorrectTrailingRanges_DifferingStarts_DifferingEnds() {
153 |
154 | let a1: ByteRange = (0..<20)
155 | let b1: ByteRange = (5..<10)
156 | let expected1: ByteRange? = nil
157 | XCTAssertEqual(a1.trailingRange(in: b1), expected1)
158 |
159 | let a2: ByteRange = (5..<10)
160 | let b2: ByteRange = (0..<20)
161 | let expected2: ByteRange? = nil
162 | XCTAssertEqual(a2.trailingRange(in: b2), expected2)
163 |
164 | let a3: ByteRange = (10..<20)
165 | let b3: ByteRange = (5..<10)
166 | let expected3: ByteRange? = nil
167 | XCTAssertEqual(a3.trailingRange(in: b3), expected3)
168 |
169 | let a4: ByteRange = (5..<10)
170 | let b4: ByteRange = (10..<20)
171 | let expected4: ByteRange? = nil
172 | XCTAssertEqual(a4.trailingRange(in: b4), expected4)
173 |
174 | }
175 |
176 | func testItComputesCorrectTrailingRanges_OffByOnes() {
177 |
178 | let a1: ByteRange = (0..<2)
179 | let b1: ByteRange = (1..<2)
180 | let expected1: ByteRange? = nil
181 | XCTAssertEqual(a1.trailingRange(in: b1), expected1)
182 |
183 | let a2: ByteRange = (1..<2)
184 | let b2: ByteRange = (0..<2)
185 | let expected2: ByteRange? = nil
186 | XCTAssertEqual(a2.trailingRange(in: b2), expected2)
187 |
188 | let a3: ByteRange = (1..<2)
189 | let b3: ByteRange = (0..<1)
190 | let expected3: ByteRange? = nil
191 | XCTAssertEqual(a3.trailingRange(in: b3), expected3)
192 |
193 | let a4: ByteRange = (0..<1)
194 | let b4: ByteRange = (1..<2)
195 | let expected4: ByteRange? = nil
196 | XCTAssertEqual(a4.trailingRange(in: b4), expected4)
197 |
198 | }
199 |
200 | // MARK: Satisfaction
201 |
202 | func testItComputesSatisfaction_SameStarts_DifferingEnds() {
203 |
204 | let a1: ByteRange = (0..<5)
205 | let b1: ByteRange = (0..<5)
206 | XCTAssertTrue(a1.fullySatisfies(b1))
207 |
208 | let a2: ByteRange = (0..<3)
209 | let b2: ByteRange = (0..<5)
210 | XCTAssertFalse(a2.fullySatisfies(b2))
211 |
212 | let a3: ByteRange = (0..<10)
213 | let b3: ByteRange = (0..<5)
214 | XCTAssertTrue(a3.fullySatisfies(b3))
215 |
216 | let a4: ByteRange = (0..<5)
217 | let b4: ByteRange = (0..<10)
218 | XCTAssertFalse(a4.fullySatisfies(b4))
219 |
220 | }
221 |
222 | func testItComputesSatisfaction_DifferingStarts_SameEnds() {
223 |
224 | let a1: ByteRange = (5..<10)
225 | let b1: ByteRange = (0..<10)
226 | XCTAssertFalse(a1.fullySatisfies(b1))
227 |
228 | let a2: ByteRange = (0..<10)
229 | let b2: ByteRange = (5..<10)
230 | XCTAssertTrue(a2.fullySatisfies(b2))
231 |
232 | }
233 |
234 | func testItComputesSatisfaction_DifferingStarts_DifferingEnds() {
235 |
236 | let a1: ByteRange = (0..<20)
237 | let b1: ByteRange = (5..<10)
238 | XCTAssertTrue(a1.fullySatisfies(b1))
239 |
240 | let a2: ByteRange = (5..<10)
241 | let b2: ByteRange = (0..<20)
242 | XCTAssertFalse(a2.fullySatisfies(b2))
243 |
244 | let a3: ByteRange = (10..<20)
245 | let b3: ByteRange = (5..<10)
246 | XCTAssertFalse(a3.fullySatisfies(b3))
247 |
248 | let a4: ByteRange = (5..<10)
249 | let b4: ByteRange = (10..<20)
250 | XCTAssertFalse(a4.fullySatisfies(b4))
251 |
252 | }
253 |
254 | func testItComputesSatisfaction_OffByOnes() {
255 |
256 | let a1: ByteRange = (0..<2)
257 | let b1: ByteRange = (1..<2)
258 | XCTAssertTrue(a1.fullySatisfies(b1))
259 |
260 | let a2: ByteRange = (1..<2)
261 | let b2: ByteRange = (0..<2)
262 | XCTAssertFalse(a2.fullySatisfies(b2))
263 |
264 | let a3: ByteRange = (1..<2)
265 | let b3: ByteRange = (0..<1)
266 | XCTAssertFalse(a3.fullySatisfies(b3))
267 |
268 | let a4: ByteRange = (0..<1)
269 | let b4: ByteRange = (1..<2)
270 | XCTAssertFalse(a4.fullySatisfies(b4))
271 |
272 | }
273 |
274 | // MARK: Continuity
275 |
276 | func testItDetectsContinuity_SameStarts_DifferingEnds() {
277 |
278 | let a1: ByteRange = (0..<5)
279 | let b1: ByteRange = (0..<5)
280 | XCTAssertFalse(a1.isContiguousWith(b1))
281 |
282 | let a2: ByteRange = (0..<3)
283 | let b2: ByteRange = (0..<5)
284 | XCTAssertFalse(a2.isContiguousWith(b2))
285 |
286 | let a3: ByteRange = (0..<10)
287 | let b3: ByteRange = (0..<5)
288 | XCTAssertFalse(a3.isContiguousWith(b3))
289 |
290 | let a4: ByteRange = (0..<5)
291 | let b4: ByteRange = (0..<10)
292 | XCTAssertFalse(a4.isContiguousWith(b4))
293 |
294 | }
295 |
296 | func testItDetectsContinuity_DifferingStarts_SameEnds() {
297 |
298 | let a1: ByteRange = (5..<10)
299 | let b1: ByteRange = (0..<10)
300 | XCTAssertFalse(a1.isContiguousWith(b1))
301 |
302 | let a2: ByteRange = (0..<10)
303 | let b2: ByteRange = (5..<10)
304 | XCTAssertFalse(a2.isContiguousWith(b2))
305 |
306 | }
307 |
308 | func testItDetectsContinuity_DifferingStarts_DifferingEnds() {
309 |
310 | let a1: ByteRange = (0..<20)
311 | let b1: ByteRange = (5..<10)
312 | XCTAssertFalse(a1.isContiguousWith(b1))
313 |
314 | let a2: ByteRange = (5..<10)
315 | let b2: ByteRange = (0..<20)
316 | XCTAssertFalse(a2.isContiguousWith(b2))
317 |
318 | let a3: ByteRange = (10..<20)
319 | let b3: ByteRange = (5..<10)
320 | XCTAssertTrue(a3.isContiguousWith(b3))
321 |
322 | let a4: ByteRange = (5..<10)
323 | let b4: ByteRange = (10..<20)
324 | XCTAssertTrue(a4.isContiguousWith(b4))
325 |
326 | }
327 |
328 | func testItDetectsContinuity_OffByOnes() {
329 |
330 | let a1: ByteRange = (0..<2)
331 | let b1: ByteRange = (1..<2)
332 | XCTAssertFalse(a1.isContiguousWith(b1))
333 |
334 | let a2: ByteRange = (1..<2)
335 | let b2: ByteRange = (0..<2)
336 | XCTAssertFalse(a2.isContiguousWith(b2))
337 |
338 | let a3: ByteRange = (1..<2)
339 | let b3: ByteRange = (0..<1)
340 | XCTAssertTrue(a3.isContiguousWith(b3))
341 |
342 | let a4: ByteRange = (0..<1)
343 | let b4: ByteRange = (1..<2)
344 | XCTAssertTrue(a4.isContiguousWith(b4))
345 |
346 | }
347 |
348 | // MARK: Combination
349 |
350 | func testItCombinesArraysOfRanges_nonOverlapping() {
351 |
352 | var input: [ByteRange];
353 | var expected: [ByteRange]
354 | var combination: [ByteRange]
355 |
356 | input = []
357 | expected = []
358 | combination = combine(input)
359 | XCTAssertEqual(combination, expected)
360 |
361 | input = [(0..<2)]
362 | expected = [(0..<2)]
363 | combination = combine(input)
364 | XCTAssertEqual(combination, expected)
365 |
366 | input = [(0..<2), (8..<10)]
367 | expected = [(0..<2), (8..<10)]
368 | combination = combine(input)
369 | XCTAssertEqual(combination, expected)
370 |
371 | input = [(0..<2), (2..<10)]
372 | expected = [(0..<10)]
373 | combination = combine(input)
374 | XCTAssertEqual(combination, expected)
375 |
376 | input = [(0..<4), (8..<10), (3..<9)]
377 | expected = [(0..<10)]
378 | combination = combine(input)
379 | XCTAssertEqual(combination, expected)
380 |
381 | input = [(0..<4), (10..<30), (20..<40), (90..<100)]
382 | expected = [(0..<4), (10..<40), (90..<100)]
383 | combination = combine(input)
384 | XCTAssertEqual(combination, expected)
385 |
386 | input = [(20..<40), (10..<30), (90..<100), (0..<4)]
387 | expected = [(0..<4), (10..<40), (90..<100)]
388 | combination = combine(input)
389 | XCTAssertEqual(combination, expected)
390 |
391 | input = [(0..<1), (2..<3)]
392 | expected = [(0..<1), (2..<3)]
393 | combination = combine(input)
394 | XCTAssertEqual(combination, expected)
395 |
396 | input = [(5..<10), (3..<11)]
397 | expected = [(3..<11)]
398 | combination = combine(input)
399 | XCTAssertEqual(combination, expected)
400 |
401 | }
402 |
403 | func testItCombinesArraysOfRanges_overlapping() {
404 |
405 | var input: [ByteRange];
406 | var expected: [ByteRange]
407 | var combination: [ByteRange]
408 |
409 | input = [(0..<10), (0..<10), (20..<40), (20..<40)]
410 | expected = [(0..<10), (20..<40)]
411 | combination = combine(input)
412 | XCTAssertEqual(combination, expected)
413 |
414 | }
415 |
416 | // MARK: Relative Position
417 |
418 | func testItYieldsCorrectRelativePositions() {
419 | let range: ByteRange = (10..<20)
420 | XCTAssertEqual(range.relativePosition(of: 0), ByteRangeIndexPosition.before)
421 | XCTAssertEqual(range.relativePosition(of: 9), ByteRangeIndexPosition.before)
422 | XCTAssertEqual(range.relativePosition(of: 10), ByteRangeIndexPosition.inside)
423 | XCTAssertEqual(range.relativePosition(of: 11), ByteRangeIndexPosition.inside)
424 | XCTAssertEqual(range.relativePosition(of: 19), ByteRangeIndexPosition.inside)
425 | XCTAssertEqual(range.relativePosition(of: 20), ByteRangeIndexPosition.after)
426 | XCTAssertEqual(range.relativePosition(of: 21), ByteRangeIndexPosition.after)
427 | }
428 |
429 | }
430 |
--------------------------------------------------------------------------------
/Sodes/SodesAudioTests/DataTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DataTests.swift
3 | // SodesAudioTests
4 | //
5 | // Created by Jared Sinclair on 8/19/16.
6 | //
7 | //
8 |
9 | import XCTest
10 | @testable import SodesAudio
11 |
12 | class DataTests: XCTestCase {
13 |
14 | func test_itReturnsSubdataForValidResponse_Variant1() {
15 | let data = Data(bytes: [0,1,2,3,4,5,6,7,8,9])
16 | let range: ByteRange = (0..<10)
17 | let subdata = data.byteRangeResponseSubdata(in: range)
18 | XCTAssertEqual(subdata, data)
19 | }
20 |
21 | func test_itReturnsSubdataForValidResponse_Variant2() {
22 | let data = Data(bytes: [0,1,2,3,4,5,6,7,8,9])
23 | let range: ByteRange = (100..<110)
24 | let subdata = data.byteRangeResponseSubdata(in: range)
25 | XCTAssertEqual(subdata, data)
26 | }
27 |
28 | func test_itReturnsNilForAnInvalidResponse_Variant1() {
29 | let data = Data(bytes: [0,1,2,3,4,5,6,7,8,9])
30 | let range: ByteRange = (0..<100)
31 | let subdata = data.byteRangeResponseSubdata(in: range)
32 | XCTAssertNil(subdata)
33 | }
34 |
35 | func test_itReturnsNilForAnInvalidResponse_Variant2() {
36 | let data = Data(bytes: [0,1,2,3,4,5,6,7,8,9])
37 | let range: ByteRange = (100..<200)
38 | let subdata = data.byteRangeResponseSubdata(in: range)
39 | XCTAssertNil(subdata)
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/Sodes/SodesAudioTests/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 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Sodes/SodesAudioTests/ResourceLoaderSubrequestTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ResourceLoaderSubrequestTests.swift
3 | // SodesAudioTests
4 | //
5 | // Created by Jared Sinclair on 8/6/16.
6 | //
7 | //
8 |
9 | import XCTest
10 | @testable import SodesAudio
11 |
12 | class ResourceLoaderSubrequestTests: XCTestCase {
13 |
14 | func testItProducesOneBigNetworkRequestWhenNoRangesAreFound() {
15 | let requestedRange: ByteRange = (10..<600)
16 | let subrequests = ResourceLoaderSubrequest.subrequests(
17 | requestedRange: requestedRange,
18 | scratchFileRanges: []
19 | )
20 | XCTAssertEqual(subrequests.count, 1)
21 | XCTAssertEqual(subrequests[0], ResourceLoaderSubrequest(source: .network, range: requestedRange))
22 | }
23 |
24 | func testItProducesOneBigScratchFileRequestWhenTheFullRangeIsSatisfied() {
25 | let requestedRange: ByteRange = (10..<600)
26 | let subrequests = ResourceLoaderSubrequest.subrequests(
27 | requestedRange: requestedRange,
28 | scratchFileRanges: [(0..<1000)]
29 | )
30 | XCTAssertEqual(subrequests.count, 1)
31 | XCTAssertEqual(subrequests[0], ResourceLoaderSubrequest(source: .scratchFile, range: requestedRange))
32 | }
33 |
34 | func testItProducesCorrectSubrequests_variant1() {
35 | let requestedRange: ByteRange = (10..<600)
36 | let subrequests = ResourceLoaderSubrequest.subrequests(
37 | requestedRange: requestedRange,
38 | scratchFileRanges: [(100..<200), (300..<400), (500..<550)]
39 | )
40 | XCTAssertEqual(subrequests.count, 7)
41 | let expected = [
42 | ResourceLoaderSubrequest(source: .network, range: (010..<100)),
43 | ResourceLoaderSubrequest(source: .scratchFile, range: (100..<200)),
44 | ResourceLoaderSubrequest(source: .network, range: (200..<300)),
45 | ResourceLoaderSubrequest(source: .scratchFile, range: (300..<400)),
46 | ResourceLoaderSubrequest(source: .network, range: (400..<500)),
47 | ResourceLoaderSubrequest(source: .scratchFile, range: (500..<550)),
48 | ResourceLoaderSubrequest(source: .network, range: (550..<600)),
49 | ]
50 | XCTAssertEqual(subrequests, expected)
51 | }
52 |
53 | func testItProducesCorrectSubrequests_variant2() {
54 | let requestedRange: ByteRange = (10..<600)
55 | let subrequests = ResourceLoaderSubrequest.subrequests(
56 | requestedRange: requestedRange,
57 | scratchFileRanges: [(100..<200), (300..<400), (500..<600)]
58 | )
59 | XCTAssertEqual(subrequests.count, 6)
60 | let expected = [
61 | ResourceLoaderSubrequest(source: .network, range: (010..<100)),
62 | ResourceLoaderSubrequest(source: .scratchFile, range: (100..<200)),
63 | ResourceLoaderSubrequest(source: .network, range: (200..<300)),
64 | ResourceLoaderSubrequest(source: .scratchFile, range: (300..<400)),
65 | ResourceLoaderSubrequest(source: .network, range: (400..<500)),
66 | ResourceLoaderSubrequest(source: .scratchFile, range: (500..<600)),
67 | ]
68 | XCTAssertEqual(subrequests, expected)
69 | }
70 |
71 | func testItProducesCorrectSubrequests_variant3() {
72 | let requestedRange: ByteRange = (0..<3)
73 | let subrequests = ResourceLoaderSubrequest.subrequests(
74 | requestedRange: requestedRange,
75 | scratchFileRanges: [(1..<2)]
76 | )
77 | XCTAssertEqual(subrequests.count, 3)
78 | let expected = [
79 | ResourceLoaderSubrequest(source: .network, range: (0..<1)),
80 | ResourceLoaderSubrequest(source: .scratchFile, range: (1..<2)),
81 | ResourceLoaderSubrequest(source: .network, range: (2..<3)),
82 | ]
83 | XCTAssertEqual(subrequests, expected)
84 | }
85 |
86 | func testItProducesCorrectSubrequests_variant4() {
87 | let requestedRange: ByteRange = (1..<2)
88 | let subrequests = ResourceLoaderSubrequest.subrequests(
89 | requestedRange: requestedRange,
90 | scratchFileRanges: [(0..<3)]
91 | )
92 | XCTAssertEqual(subrequests.count, 1)
93 | let expected = [
94 | ResourceLoaderSubrequest(source: .scratchFile, range: (1..<2)),
95 | ]
96 | XCTAssertEqual(subrequests, expected)
97 | }
98 |
99 | func testItProducesCorrectSubrequests_variant5() {
100 | let requestedRange: ByteRange = (0..<10)
101 | let subrequests = ResourceLoaderSubrequest.subrequests(
102 | requestedRange: requestedRange,
103 | scratchFileRanges: [(0..<5)]
104 | )
105 | XCTAssertEqual(subrequests.count, 2)
106 | let expected = [
107 | ResourceLoaderSubrequest(source: .scratchFile, range: (0..<5)),
108 | ResourceLoaderSubrequest(source: .network, range: (5..<10)),
109 | ]
110 | XCTAssertEqual(subrequests, expected)
111 | }
112 |
113 | func testItProducesCorrectSubrequests_variant6() {
114 | let requestedRange: ByteRange = (2..<10)
115 | let subrequests = ResourceLoaderSubrequest.subrequests(
116 | requestedRange: requestedRange,
117 | scratchFileRanges: [(0..<2), (2..<5), (4..<10)]
118 | )
119 | XCTAssertEqual(subrequests.count, 2)
120 | let expected = [
121 | ResourceLoaderSubrequest(source: .scratchFile, range: (2..<5)),
122 | ResourceLoaderSubrequest(source: .scratchFile, range: (5..<10)),
123 | ]
124 | XCTAssertEqual(subrequests, expected)
125 | }
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/Sodes/SodesAudioTests/URLRequestTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLRequestTests.swift
3 | // SodesAudioTests
4 | //
5 | // Created by Jared Sinclair on 8/19/16.
6 | //
7 | //
8 |
9 | import XCTest
10 | @testable import SodesAudio
11 |
12 | class URLRequestTests: XCTestCase {
13 |
14 | func test_itCreatesADataRequest() {
15 | let url = URL(string: "http://example.com/audio.mp3")!
16 | let range: ByteRange = (100..<200)
17 | let request = URLRequest.dataRequest(from: url, for: range)
18 | XCTAssertEqual(request.cachePolicy, .reloadIgnoringLocalCacheData)
19 | XCTAssertEqual(request.url, url)
20 | XCTAssertEqual(request.allHTTPHeaderFields!["Range"], "bytes=100-199")
21 | }
22 |
23 | func test_itExtractsRequestedByteRange() {
24 | let url = URL(string: "http://example.com/audio.mp3")!
25 | let range: ByteRange = (100..<200)
26 | let request = URLRequest.dataRequest(from: url, for: range)
27 | XCTAssertEqual(request.byteRange, range)
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/Sodes/SodesAudioTests/URLResponseTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // URLResponseTests.swift
3 | // SodesAudioTests
4 | //
5 | // Created by Jared Sinclair on 8/20/16.
6 | //
7 | //
8 |
9 | import XCTest
10 | @testable import SodesAudio
11 |
12 | class URLResponseTests: XCTestCase {
13 |
14 | func test_itExtractsResponseRangeAndTotalExpectedContentLength() {
15 | let url = URL(string: "http://example.com/audio.mp3")!
16 | let response = HTTPURLResponse(
17 | url: url,
18 | statusCode: 206,
19 | httpVersion: nil,
20 | headerFields: [
21 | "Content-Range": "bytes 100-199/444444"
22 | ]
23 | )
24 | let expectedRange: ByteRange = (100..<200)
25 | XCTAssertEqual(response?.sodes_responseRange, expectedRange)
26 | XCTAssertEqual(response?.sodes_expectedContentLength, 444444)
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/Sodes/SodesExample/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // SodesExample
4 | //
5 | // Created by Jared Sinclair on 9/2/16.
6 | //
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 | var window: UIWindow?
15 |
16 |
17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
18 | // Override point for customization after application launch.
19 | return true
20 | }
21 |
22 | func applicationWillResignActive(_ application: UIApplication) {
23 | // 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.
24 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
25 | }
26 |
27 | func applicationDidEnterBackground(_ application: UIApplication) {
28 | // 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.
29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
30 | }
31 |
32 | func applicationWillEnterForeground(_ application: UIApplication) {
33 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
34 | }
35 |
36 | func applicationDidBecomeActive(_ application: UIApplication) {
37 | // 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.
38 | }
39 |
40 | func applicationWillTerminate(_ application: UIApplication) {
41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
42 | }
43 |
44 |
45 | }
46 |
47 |
--------------------------------------------------------------------------------
/Sodes/SodesExample/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "29x29",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "29x29",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "40x40",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "40x40",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "60x60",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "60x60",
31 | "scale" : "3x"
32 | }
33 | ],
34 | "info" : {
35 | "version" : 1,
36 | "author" : "xcode"
37 | }
38 | }
--------------------------------------------------------------------------------
/Sodes/SodesExample/Base.lproj/LaunchScreen.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 |
--------------------------------------------------------------------------------
/Sodes/SodesExample/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 |
30 |
31 |
32 |
33 |
38 |
43 |
44 |
45 |
54 |
63 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/Sodes/SodesExample/EpisodeDuration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // EpisodeDuration.swift
3 | // Sodes
4 | //
5 | // Created by Jared Sinclair on 8/28/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | public struct EpisodeDurationParsing {
12 |
13 | private static let lock = NSLock()
14 | private static let calendar = Calendar(identifier: .gregorian)
15 |
16 | private static let hourMinuteSecondFormatter: DateFormatter = {
17 | let formatter = DateFormatter()
18 | formatter.dateFormat = "HH:mm:ss"
19 | return formatter
20 | }()
21 |
22 | private static let minuteSecondFormatter: DateFormatter = {
23 | let formatter = DateFormatter()
24 | formatter.dateFormat = "mm:ss"
25 | return formatter
26 | }()
27 |
28 | public static func duration(from string: String) -> TimeInterval? {
29 |
30 | let comps = string.components(separatedBy: ":").filter{!$0.isEmpty}
31 |
32 | guard !comps.isEmpty else {return nil}
33 |
34 | let duration: TimeInterval
35 |
36 | if comps.count == 1 && string.range(of: ":") == nil {
37 | duration = comps[0].toSeconds(constrainToClockRange: false)
38 | }
39 | else if comps.count == 2 {
40 | guard comps[0].characters.count == 2 else {return nil}
41 | guard comps[1].characters.count == 2 else {return nil}
42 | duration =
43 | comps[0].toMinutes() * 60
44 | + comps[1].toSeconds(constrainToClockRange: true)
45 | }
46 | else if comps.count == 3 {
47 | guard comps[1].characters.count == 2 else {return nil}
48 | guard comps[2].characters.count == 2 else {return nil}
49 | duration =
50 | comps[0].toHours() * 3600
51 | + comps[1].toMinutes() * 60
52 | + comps[2].toSeconds(constrainToClockRange: true)
53 | }
54 | else {
55 | duration = 0
56 | }
57 |
58 | return (duration > 0) ? duration : nil
59 |
60 | }
61 |
62 | public static func string(from duration: TimeInterval) -> String {
63 |
64 | guard duration > 0 else {return "00:00"}
65 | guard duration <= (23*3600 + 59*60 + 59) else {return "23:59:59"}
66 |
67 | let hours = floor(duration / 3600)
68 | let minutesAndSeconds = duration.truncatingRemainder(dividingBy: 3600)
69 | let minutes = floor(minutesAndSeconds / 60)
70 | let seconds = floor(minutesAndSeconds.truncatingRemainder(dividingBy: 60))
71 |
72 | var comps = DateComponents()
73 | comps.calendar = calendar
74 | comps.day = 1
75 | comps.month = 1
76 | comps.year = 2016
77 | comps.hour = Int(hours)
78 | comps.minute = Int(minutes)
79 | comps.second = Int(seconds)
80 |
81 | guard let date = comps.date else {return "00:00"}
82 |
83 | if hours > 0 {
84 | lock.lock()
85 | let result = hourMinuteSecondFormatter.string(from: date)
86 | lock.unlock()
87 | return result
88 | } else {
89 | lock.lock()
90 | let result = minuteSecondFormatter.string(from: date)
91 | lock.unlock()
92 | return result
93 | }
94 |
95 | }
96 |
97 | private init() {}
98 |
99 | }
100 |
101 | private extension String {
102 |
103 | func toSeconds(constrainToClockRange: Bool) -> TimeInterval {
104 | guard let possible = TimeInterval(self) else {return 0}
105 | if (constrainToClockRange) {
106 | return (0 <= possible && possible <= 59) ? possible : 0
107 | } else {
108 | return possible
109 | }
110 | }
111 |
112 | func toMinutes() -> TimeInterval {
113 | guard let possible = TimeInterval(self) else {return 0}
114 | return (0 <= possible && possible <= 59) ? possible : 0
115 | }
116 |
117 | func toHours() -> TimeInterval {
118 | return TimeInterval(self) ?? 0
119 | }
120 |
121 | }
122 |
123 | private func *(lhs: TimeInterval?, rhs: TimeInterval) -> TimeInterval {
124 | return (lhs ?? 0) * rhs
125 | }
126 |
--------------------------------------------------------------------------------
/Sodes/SodesExample/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 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | APPL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | NSAppTransportSecurity
24 |
25 | NSAllowsArbitraryLoads
26 |
27 |
28 | UILaunchStoryboardName
29 | LaunchScreen
30 | UIMainStoryboardFile
31 | Main
32 | UIRequiredDeviceCapabilities
33 |
34 | armv7
35 |
36 | UISupportedInterfaceOrientations
37 |
38 | UIInterfaceOrientationPortrait
39 | UIInterfaceOrientationLandscapeLeft
40 | UIInterfaceOrientationLandscapeRight
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/Sodes/SodesExample/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // SodesExample
4 | //
5 | // Created by Jared Sinclair on 9/2/16.
6 | //
7 | //
8 |
9 | import UIKit
10 | import SodesAudio
11 | import MediaPlayer
12 |
13 | struct TestSource: PlaybackSource {
14 | let uniqueId: String = "abcxyz"
15 | var artistId: String = "123456"
16 | var remoteUrl: URL = URL(string: "http://content.blubrry.com/exponent/exponent86.mp3")!
17 | var title: String? = "Track Title"
18 | var albumTitle: String? = "Album Title"
19 | var artist: String? = "Artist"
20 | var artworkUrl: URL? = URL(string: "http://exponent.fm/wp-content/uploads/2014/02/cropped-Exponent-header.png")
21 | var mediaType: MPMediaType = .podcast
22 | var expectedLengthInBytes: Int64? = nil
23 | }
24 |
25 | class ViewController: UIViewController {
26 |
27 | @IBOutlet private var playPauseButton: UIButton!
28 | @IBOutlet private var backButton: UIButton!
29 | @IBOutlet private var forwardButton: UIButton!
30 | @IBOutlet private var progressBar: UIProgressView!
31 | @IBOutlet private var elapsedTimeLabel: UILabel!
32 | @IBOutlet private var remainingTimeLabel: UILabel!
33 | @IBOutlet private var activityIndicator: UIActivityIndicatorView!
34 | @IBOutlet private var byteRangeTextView: UITextView!
35 |
36 | override func viewDidLoad() {
37 | super.viewDidLoad()
38 |
39 | let source = TestSource()
40 | PlaybackController.sharedController.prepare(source, startTime: 0, playWhenReady: true)
41 |
42 | let center = NotificationCenter.default
43 |
44 | center.addObserver(forName: PlaybackControllerNotification.DidUpdateElapsedTime.name, object: nil, queue: .main) { (note) in
45 | let controller = PlaybackController.sharedController
46 | guard let duration = controller.duration else {return}
47 | let elapsed = floor(controller.elapsedTime)
48 | self.elapsedTimeLabel.text = EpisodeDurationParsing.string(from: elapsed)
49 | let remaining = floor(duration - elapsed)
50 | self.remainingTimeLabel.text = EpisodeDurationParsing.string(from: remaining)
51 | self.progressBar.progress = Float(elapsed / duration)
52 | }
53 |
54 | center.addObserver(forName: PlaybackControllerNotification.DidUpdateStatus.name, object: nil, queue: .main) { (note) in
55 | switch PlaybackController.sharedController.status {
56 | case .buffering, .preparing(_,_):
57 | self.activityIndicator.startAnimating()
58 | default:
59 | self.activityIndicator.stopAnimating()
60 | }
61 | }
62 |
63 | center.addObserver(forName: PlaybackControllerNotification.DidUpdateLoadedByteRanges.name, object: nil, queue: .main) { (note) in
64 | let key = PlaybackControllerNotification.ByteRangesKey
65 | if let ranges = note.userInfo?[key] as? [ByteRange] {
66 | self.byteRangeTextView.text = "\(ranges)"
67 | }
68 | }
69 |
70 | }
71 |
72 | @IBAction func togglePlayPause(sender: AnyObject?) {
73 | PlaybackController.sharedController.togglePlayPause()
74 | }
75 |
76 | @IBAction func back(sender: AnyObject?) {
77 | PlaybackController.sharedController.skipBackward()
78 | }
79 |
80 | @IBAction func forward(sender: AnyObject?) {
81 | PlaybackController.sharedController.skipForward()
82 | }
83 |
84 | }
85 |
--------------------------------------------------------------------------------
/Sodes/SodesFoundation/AsyncBlockOperation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AsyncBlockOperation.swift
3 | // SodesFoundation
4 | //
5 | // Created by Jared Sinclair on 7/9/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | public class AsyncBlockOperation: WorkOperation {
12 |
13 | public typealias FinishHandler = () -> Void
14 | public typealias WorkBlock = (@escaping FinishHandler) -> Void
15 |
16 | private let workBlock: WorkBlock
17 |
18 | public init(work: @escaping WorkBlock) {
19 | self.workBlock = work
20 | super.init{}
21 | }
22 |
23 | public init(work: @escaping WorkBlock, completion: @escaping () -> Void) {
24 | self.workBlock = work
25 | super.init(completion: completion)
26 | }
27 |
28 | override public func work(_ finish: @escaping FinishHandler) {
29 | workBlock(finish)
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/Sodes/SodesFoundation/DelegatedHTTPOperation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DelegatedHTTPOperation.swift
3 | // SodesFoundation
4 | //
5 | // Created by Jared Sinclair on 8/6/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | public protocol HTTPOperationDataDelegate: class {
12 | func delegatedHTTPOperation(_ operation: DelegatedHTTPOperation, didReceiveResponse response: HTTPURLResponse)
13 | func delegatedHTTPOperation(_ operation: DelegatedHTTPOperation, didReceiveData data: Data)
14 | }
15 |
16 | public class DelegatedHTTPOperation: WorkOperation {
17 |
18 | public enum Result {
19 | case success(response: HTTPURLResponse, bytesExpected: Int64, bytesReceived: Int64)
20 | case error(HTTPURLResponse?, Error?)
21 | }
22 |
23 | public let request: URLRequest
24 |
25 | fileprivate let delegateQueue: OperationQueue
26 | fileprivate weak var dataDelegate: HTTPOperationDataDelegate?
27 | fileprivate let internals: DelegatedHTTPOperationInternals
28 | fileprivate var session: URLSession!
29 | fileprivate var task: URLSessionDataTask!
30 |
31 | fileprivate var finish: (() -> Void)?
32 | fileprivate var response: HTTPURLResponse? = nil
33 | fileprivate var bytesExpected: Int64 = 0
34 | fileprivate var bytesReceived: Int64 = 0
35 |
36 | public init(url: URL, configuration: URLSessionConfiguration = .default, dataDelegate: HTTPOperationDataDelegate, delegateQueue: OperationQueue, completion: @escaping (Result) -> Void) {
37 | assert(delegateQueue.maxConcurrentOperationCount == 1, "DelegatedHTTPOperation's delegate queue must be a serial queue.")
38 | let internals = DelegatedHTTPOperationInternals()
39 | self.internals = internals
40 | self.delegateQueue = delegateQueue
41 | self.dataDelegate = dataDelegate
42 | self.request = URLRequest(url: url)
43 | super.init { completion(internals.result) }
44 | self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: delegateQueue)
45 | self.task = session.dataTask(with: request)
46 | }
47 |
48 | public init(request: URLRequest, configuration: URLSessionConfiguration = .default, dataDelegate: HTTPOperationDataDelegate, delegateQueue: OperationQueue, completion: @escaping (Result) -> Void) {
49 | assert(delegateQueue.maxConcurrentOperationCount == 1, "DelegatedHTTPOperation's delegate queue must be a serial queue.")
50 | let internals = DelegatedHTTPOperationInternals()
51 | self.internals = internals
52 | self.delegateQueue = delegateQueue
53 | self.dataDelegate = dataDelegate
54 | self.request = request
55 | super.init { completion(internals.result) }
56 | self.session = URLSession(configuration: configuration, delegate: self, delegateQueue: delegateQueue)
57 | self.task = session.dataTask(with: request)
58 | }
59 |
60 | override public func work(_ finish: @escaping () -> Void) {
61 | self.finish = finish
62 | task.resume()
63 | }
64 |
65 | override public func cancel() {
66 | task.cancel()
67 | super.cancel()
68 | }
69 |
70 | fileprivate func invokeFinishHandler() {
71 | delegateQueue.addOperation { [weak self] in
72 | guard let this = self else {return}
73 | guard !this.isCancelled else {return}
74 | let handler = this.finish
75 | this.finish = nil
76 | handler?()
77 | }
78 | }
79 |
80 | }
81 |
82 | extension DelegatedHTTPOperation: URLSessionDataDelegate {
83 |
84 | public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
85 |
86 | guard !isCancelled else {return}
87 |
88 | // TODO: [MEDIUM] Consider how to handle redirects (POST vs GET e.g.)
89 | guard let httpResponse = response as? HTTPURLResponse, 200 <= httpResponse.statusCode, httpResponse.statusCode <= 299 else
90 | {
91 | SodesLog("Invalid response received: \(response)")
92 | completionHandler(.cancel)
93 | internals.result = .error((response as? HTTPURLResponse), nil)
94 | invokeFinishHandler()
95 | return
96 | }
97 |
98 | self.response = httpResponse
99 | self.bytesExpected = httpResponse.contentLength
100 | SodesLog("self.bytesExpected: \(bytesExpected)\nresponse: \(httpResponse)")
101 |
102 | delegateQueue.addOperation { [weak self] in
103 | guard let this = self else {return}
104 | guard !this.isCancelled else {return}
105 | this.dataDelegate?.delegatedHTTPOperation(this, didReceiveResponse: httpResponse)
106 | }
107 |
108 | completionHandler(.allow)
109 | }
110 |
111 | public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
112 | guard !isCancelled else {return}
113 | bytesReceived += Int64(data.count)
114 | delegateQueue.addOperation { [weak self] in
115 | guard let this = self else {return}
116 | guard !this.isCancelled else {return}
117 | this.dataDelegate?.delegatedHTTPOperation(this, didReceiveData: data)
118 | }
119 | }
120 |
121 | public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
122 | guard !isCancelled else {return}
123 | if let response = self.response, error == nil {
124 | internals.result = .success(
125 | response: response,
126 | bytesExpected: bytesExpected,
127 | bytesReceived: bytesReceived
128 | )
129 | } else {
130 | internals.result = .error(response, error)
131 | }
132 | invokeFinishHandler()
133 | }
134 |
135 | }
136 |
137 | fileprivate class DelegatedHTTPOperationInternals {
138 | var result: DelegatedHTTPOperation.Result = .error(nil, nil)
139 | }
140 |
141 | fileprivate extension HTTPURLResponse {
142 | var contentLength: Int64 {
143 | if let s = allHeaderFields["Content-Length"] as? String {
144 | return Int64(s) ?? 0
145 | }
146 | return allHeaderFields["Content-Length"] as? Int64 ?? 0
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Sodes/SodesFoundation/DownloadOperation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DownloadOperation.swift
3 | // SodesFoundation
4 | //
5 | // Created by Jared Sinclair on 7/9/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | public class DownloadOperation: WorkOperation {
12 |
13 | public enum Result {
14 | case tempUrl(URL, HTTPURLResponse)
15 | case error(HTTPURLResponse?, Error?)
16 | }
17 |
18 | private let internals: DownloadOperationInternals
19 |
20 | public init(url: URL, session: URLSession, completion: @escaping (Result) -> Void) {
21 | let internals = DownloadOperationInternals(
22 | request: URLRequest(url: url),
23 | session: session
24 | )
25 | self.internals = internals
26 | super.init {
27 | completion(internals.result)
28 | }
29 | }
30 |
31 | public init(request: URLRequest, session: URLSession, completion: @escaping (Result) -> Void) {
32 | let internals = DownloadOperationInternals(
33 | request: request,
34 | session: session
35 | )
36 | self.internals = internals
37 | super.init {
38 | completion(internals.result)
39 | }
40 | }
41 |
42 | override public func work(_ finish: @escaping () -> Void) {
43 | internals.task = internals.session.downloadTask(with: internals.request) { (tempUrl, response, error) in
44 | guard !self.isCancelled else {return}
45 | // TODO: [MEDIUM] Consider how to handle redirects (POST vs GET e.g.)
46 | guard
47 | let r = response as? HTTPURLResponse,
48 | let tempUrl = tempUrl,
49 | 200 <= r.statusCode && r.statusCode <= 299 else
50 | {
51 | self.internals.result = .error((response as? HTTPURLResponse), error)
52 | finish()
53 | return
54 | }
55 | self.internals.result = .tempUrl(tempUrl, r)
56 | finish()
57 | }
58 | internals.task?.resume()
59 | }
60 |
61 | override public func cancel() {
62 | internals.task?.cancel()
63 | super.cancel()
64 | }
65 |
66 | }
67 |
68 | private class DownloadOperationInternals {
69 |
70 | let request: URLRequest
71 | let session: URLSession
72 | var task: URLSessionDownloadTask?
73 | var result: DownloadOperation.Result = .error(nil, nil)
74 |
75 | init(request: URLRequest, session: URLSession) {
76 | self.request = request
77 | self.session = session
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/Sodes/SodesFoundation/Error.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Error.swift
3 | // Sodes
4 | //
5 | // Created by Jared Sinclair on 8/30/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | public func description(of optionalError: Error?) -> String {
12 | if let error = optionalError {
13 | let type = type(of: error)
14 | if type == NSError.self {
15 | let nsError = error as NSError
16 | return nsError.domain + "_\(nsError.code)"
17 | } else {
18 | return String(describing: type) + "." + String(describing: error)
19 | }
20 | } else {
21 | return "nil"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sodes/SodesFoundation/FileManager.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileManagement.swift
3 | // SodesFoundation
4 | //
5 | // Created by Jared Sinclair on 7/9/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | public extension FileManager {
12 |
13 | public func cachesDirectory() -> URL? {
14 | let directories = urls(
15 | for: .cachesDirectory,
16 | in: .userDomainMask
17 | )
18 | return directories.first
19 | }
20 |
21 | public func documentsDirectory() -> URL? {
22 | let directories = urls(
23 | for: .documentDirectory,
24 | in: .userDomainMask
25 | )
26 | return directories.first
27 | }
28 |
29 | public func createDirectoryAt(_ url: URL) -> Bool {
30 | do {
31 | try createDirectory(
32 | at: url,
33 | withIntermediateDirectories: true,
34 | attributes: nil
35 | )
36 | }
37 | catch {
38 | return false
39 | }
40 | return true
41 | }
42 |
43 | public func createSubdirectory(_ name: String, atUrl url: URL) -> Bool {
44 | let subdirectoryUrl = url.appendingPathComponent(name, isDirectory: true)
45 | return self.createDirectoryAt(subdirectoryUrl)
46 | }
47 |
48 | public func removeDirectory(_ directory: URL) -> Bool {
49 | do {
50 | try removeItem(at: directory)
51 | }
52 | catch {
53 | return false
54 | }
55 | return true
56 | }
57 |
58 | public func removeFile(at url: URL) -> Bool {
59 | do {
60 | try removeItem(at: url)
61 | }
62 | catch {
63 | return false
64 | }
65 | return true
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/Sodes/SodesFoundation/GCD.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GCD.swift
3 | // SodesFoundation
4 | //
5 | // Created by Jared Sinclair on 7/9/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | public func onMainQueue(_ block: @escaping () -> Void) {
12 | DispatchQueue.main.async(execute: block)
13 | }
14 |
15 | public func onMainQueueSyncIfPossible(_ block: @escaping () -> Void) {
16 | if OperationQueue.current === OperationQueue.main {
17 | block()
18 | } else {
19 | DispatchQueue.main.async(execute: block)
20 | }
21 | }
22 |
23 | public func onGlobalQueue(_ block: @escaping () -> Void) {
24 | DispatchQueue.global().async(execute: block)
25 | }
26 |
--------------------------------------------------------------------------------
/Sodes/SodesFoundation/HTTPOperation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HTTPOperation.swift
3 | // SodesFoundation
4 | //
5 | // Created by Jared Sinclair on 7/9/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | public class HTTPOperation: WorkOperation {
12 |
13 | public enum Result {
14 | case data(Data, HTTPURLResponse)
15 | case error(HTTPURLResponse?, Error?)
16 | }
17 |
18 | private let internals: HTTPOperationInternals
19 |
20 | public init(url: URL, session: URLSession, completion: @escaping (Result) -> Void) {
21 | let internals = HTTPOperationInternals(
22 | request: URLRequest(url: url),
23 | session: session
24 | )
25 | self.internals = internals
26 | super.init {
27 | completion(internals.result)
28 | }
29 | }
30 |
31 | public init(request: URLRequest, session: URLSession, completion: @escaping (Result) -> Void) {
32 | let internals = HTTPOperationInternals(
33 | request: request,
34 | session: session
35 | )
36 | self.internals = internals
37 | super.init {
38 | completion(internals.result)
39 | }
40 | }
41 |
42 | override public func work(_ finish: @escaping () -> Void) {
43 | internals.task = internals.session.dataTask(with: internals.request) { (data, response, error) in
44 | guard !self.isCancelled else {return}
45 | // TODO: [MEDIUM] Consider how to handle redirects (POST vs GET e.g.)
46 | guard let r = response as? HTTPURLResponse, let data = data, 200 <= r.statusCode, r.statusCode <= 299 else
47 | {
48 | self.internals.result = .error((response as? HTTPURLResponse), error)
49 | finish()
50 | return
51 | }
52 | self.internals.result = .data(data, r)
53 | finish()
54 | }
55 | internals.task?.resume()
56 | }
57 |
58 | override public func cancel() {
59 | internals.task?.cancel()
60 | super.cancel()
61 | }
62 |
63 | }
64 |
65 | private class HTTPOperationInternals {
66 |
67 | let request: URLRequest
68 | let session: URLSession
69 | var task: URLSessionDataTask?
70 | var result: HTTPOperation.Result = .error(nil, nil)
71 |
72 | init(request: URLRequest, session: URLSession) {
73 | self.request = request
74 | self.session = session
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/Sodes/SodesFoundation/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 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Sodes/SodesFoundation/NSDateFormatter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSDateFormatter.swift
3 | // SodesCore
4 | //
5 | // Created by Jared Sinclair on 7/10/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | public class RFC822Formatter {
12 |
13 | public static let sharedFormatter = RFC822Formatter()
14 |
15 | private let formatterWithWeekdays: DateFormatter
16 | private let formatterWithoutWeekdays: DateFormatter
17 | private let lock = NSLock()
18 |
19 | public init() {
20 | formatterWithWeekdays = DateFormatter.RFC822Formatter(includeWeekdays: true)
21 | formatterWithoutWeekdays = DateFormatter.RFC822Formatter(includeWeekdays: false)
22 | }
23 |
24 | public func string(from date: Date) -> String? {
25 | lock.lock()
26 | let withWeekdays = formatterWithWeekdays.string(from: date)
27 | lock.unlock()
28 | return withWeekdays
29 | }
30 |
31 | public func date(from string: String) -> Date? {
32 |
33 | // TODO: [MEDIUM] Ensure that we don't need a more efficient means of knowing
34 | // which formatter to try first (or not at all), etc.
35 |
36 | lock.lock()
37 | let withWeekdays = formatterWithWeekdays.date(from: string)
38 | lock.unlock()
39 | if let with = withWeekdays {
40 | return with
41 | }
42 |
43 | lock.lock()
44 | let withoutWeekdays = formatterWithoutWeekdays.date(from: string)
45 | lock.unlock()
46 | return withoutWeekdays
47 |
48 | }
49 |
50 | }
51 |
52 | private extension DateFormatter {
53 |
54 | static func RFC822Formatter(includeWeekdays include: Bool) -> DateFormatter {
55 | let formatter = DateFormatter()
56 | formatter.locale = Locale(identifier: "en_US_POSIX")
57 | if include {
58 | formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss z"
59 | } else {
60 | formatter.dateFormat = "dd MMM yyyy HH:mm:ss z"
61 | }
62 | return formatter
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/Sodes/SodesFoundation/SodesLog.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SodesLog.swift
3 | // SodesFoundation
4 | //
5 | // Created by Jared Sinclair on 7/25/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | public func SodesLog(_ input: Any = "", file: String = #file, function: String = #function, line: Int = #line) {
12 | #if DEBUG
13 | print("\n\(NSDate())\n\(file):\n\(function)() Line \(line)\n\(input)\n\n")
14 | #endif
15 | }
16 |
--------------------------------------------------------------------------------
/Sodes/SodesFoundation/Throttle.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Throttle.swift
3 | // SodesFoundation
4 | //
5 | // Created by Jared Sinclair on 8/11/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | public class Throttle {
12 |
13 | public typealias Action = () -> Void
14 |
15 | fileprivate let serialQueue: OperationQueue
16 | fileprivate var pendingAction: Action?
17 | fileprivate var hasTakenActionAtLeastOnce = false
18 | fileprivate var timer: Timer?
19 | fileprivate var timerTarget: TimerTarget! {
20 | didSet { assert(oldValue == nil) }
21 | }
22 |
23 | public init(minimumInterval: TimeInterval, qualityOfService: QualityOfService) {
24 | serialQueue = {
25 | let queue = OperationQueue()
26 | queue.qualityOfService = qualityOfService
27 | queue.maxConcurrentOperationCount = 1
28 | return queue
29 | }()
30 | timerTarget = TimerTarget(delegate: self)
31 | timer = {
32 | let timer = Timer(
33 | timeInterval: minimumInterval,
34 | target: timerTarget,
35 | selector: #selector(TimerTarget.timerFired),
36 | userInfo: nil,
37 | repeats: true
38 | )
39 | timer.tolerance = minimumInterval * 0.5
40 | RunLoop.main.add(timer, forMode: .commonModes)
41 | return timer
42 | }()
43 | }
44 |
45 | deinit {
46 | timer?.invalidate()
47 | timer = nil
48 | }
49 |
50 | public func enqueue(immediately: Bool = false, action: @escaping Action) {
51 | let op = BlockOperation { [weak self] in
52 | guard let this = self else {return}
53 | if !this.hasTakenActionAtLeastOnce || immediately {
54 | this.hasTakenActionAtLeastOnce = true
55 | this.pendingAction = nil
56 | action()
57 | } else {
58 | this.pendingAction = action
59 | }
60 | }
61 | op.queuePriority = immediately ? .veryHigh : .normal
62 | serialQueue.addOperation(op)
63 | }
64 |
65 | }
66 |
67 | extension Throttle: TimerTargetDelegate {
68 |
69 | fileprivate func timerFired(for: TimerTarget) {
70 | serialQueue.addOperation { [weak self] in
71 | guard let this = self else {return}
72 | guard let action = this.pendingAction else {return}
73 | this.pendingAction = nil
74 | action()
75 | }
76 | }
77 |
78 | }
79 |
80 | private protocol TimerTargetDelegate: class {
81 | func timerFired(for: TimerTarget)
82 | }
83 |
84 | private class TimerTarget {
85 |
86 | weak var delegate: TimerTargetDelegate?
87 |
88 | init(delegate: TimerTargetDelegate) {
89 | self.delegate = delegate
90 | }
91 |
92 | @objc func timerFired(_ timer: Timer) {
93 | delegate?.timerFired(for: self)
94 | }
95 |
96 | }
97 |
--------------------------------------------------------------------------------
/Sodes/SodesFoundation/WorkOperation.swift:
--------------------------------------------------------------------------------
1 | //
2 | // WorkOperation.swift
3 | // SodesFoundation
4 | //
5 | // Created by Jared Sinclair on 7/9/16.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | open class WorkOperation: Operation {
12 |
13 | // MARK: Typealiases
14 |
15 | public typealias FinishHandler = () -> Void
16 |
17 | // MARK: Private/public Properties
18 |
19 | private let performQueue: DispatchQueue
20 | private let completion: () -> Void
21 |
22 | // MARK: Init
23 |
24 | public init(completion: @escaping () -> Void) {
25 | self.completion = completion
26 | self.performQueue = DispatchQueue(
27 | label: "com.niceboy.SodesCore.WorkOperation",
28 | qos: .background
29 | )
30 | super.init()
31 | }
32 |
33 | // MARK: Required Methods for Subclasses
34 |
35 | open func work(_ finish: @escaping () -> Void) {
36 | assertionFailure("Subclasses must override without calling super.")
37 | }
38 |
39 | // MARK: NSOperation Requirements
40 |
41 | override open func start() {
42 | guard !isCancelled else {return}
43 | markAsRunning()
44 | performQueue.async {
45 | self.work { (result) in
46 | onMainQueue {
47 | guard !self.isCancelled else {return}
48 | self.completion()
49 | self.markAsFinished()
50 | }
51 | }
52 | }
53 | }
54 |
55 | private var _finished: Bool = false
56 | override open var isFinished: Bool {
57 | get { return _finished }
58 | set { _finished = newValue }
59 | }
60 |
61 | private var _executing: Bool = false
62 | override open var isExecuting: Bool {
63 | get { return _executing }
64 | set { _executing = newValue }
65 | }
66 |
67 | override open var isAsynchronous: Bool {
68 | return true
69 | }
70 |
71 | private func markAsRunning() {
72 | willChangeValue(for: .isExecuting)
73 | _executing = true
74 | didChangeValue(for: .isExecuting)
75 | }
76 |
77 | private func markAsFinished() {
78 | willChangeValue(for: .isExecuting)
79 | willChangeValue(for: .isFinished)
80 | _executing = false
81 | _finished = true
82 | didChangeValue(for: .isExecuting)
83 | didChangeValue(for: .isFinished)
84 | }
85 |
86 | private func willChangeValue(for key: OperationChangeKey) {
87 | self.willChangeValue(forKey: key.rawValue)
88 | }
89 |
90 | private func didChangeValue(for key: OperationChangeKey) {
91 | self.didChangeValue(forKey: key.rawValue)
92 | }
93 |
94 | }
95 |
96 | private enum OperationChangeKey: String {
97 | case isFinished
98 | case isExecuting
99 | }
100 |
--------------------------------------------------------------------------------
/Sodes/SodesFoundationTests/DateFormattingTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DateFormattingTests.swift
3 | // Sodes
4 | //
5 | // Created by Jared Sinclair on 8/17/16.
6 | //
7 | //
8 |
9 | import XCTest
10 | @testable import SodesFoundation
11 |
12 | class DateFormattingTests: XCTestCase {
13 |
14 | func test_itParsesDateStringWithWeekday() {
15 | let expected = Date(timeIntervalSince1970: 1468130424)
16 | let formatter = RFC822Formatter.sharedFormatter
17 | let actual = formatter.date(from: "Sun, 10 Jul 2016 06:00:24 +0000")
18 | XCTAssertEqual(expected, actual)
19 | }
20 |
21 | func test_itParsesDateStringWithoutWeekday() {
22 | let expected = Date(timeIntervalSince1970: 1468130424)
23 | let formatter = RFC822Formatter.sharedFormatter
24 | let actual = formatter.date(from: "10 Jul 2016 06:00:24 +0000")
25 | XCTAssertEqual(expected, actual)
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/Sodes/SodesFoundationTests/DelegatedHTTPOperationTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DelegatedHTTPOperationTests.swift
3 | // SodesFoundationTests
4 | //
5 | // Created by Jared Sinclair on 8/7/16.
6 | //
7 | //
8 |
9 | import XCTest
10 | @testable import SodesFoundation
11 |
12 | class DelegatedHTTPOperationTests: XCTestCase {
13 |
14 | var receivedBytes: Int64 = 0
15 | var delegateQueue: OperationQueue!
16 |
17 | override func setUp() {
18 | receivedBytes = 0
19 | delegateQueue = OperationQueue()
20 | delegateQueue.maxConcurrentOperationCount = 1
21 | }
22 |
23 | func testItFetchesAKnownGoodResource() {
24 | let url = URL(string: "http://jaredsinclair.com/img/pixel-jared.png")!
25 | let exp = expectation(description: "It fetches a known good resource.")
26 | let op = DelegatedHTTPOperation(
27 | url: url,
28 | configuration: .default,
29 | dataDelegate: self,
30 | delegateQueue: delegateQueue,
31 | completion: { (result) in
32 | XCTAssertEqual(OperationQueue.current, OperationQueue.main)
33 | switch result {
34 | case .success(_, let expected, let received):
35 | XCTAssertEqual(expected, received)
36 | XCTAssertEqual(self.receivedBytes, received)
37 | break
38 | case .error(let r, let e):
39 | SodesLog("response: \(r), error: \(e)")
40 | XCTFail()
41 | break
42 | }
43 | XCTAssertEqual(self.receivedBytes, 2134)
44 | exp.fulfill()
45 | })
46 | op.start()
47 | waitForExpectations(timeout: 10)
48 | }
49 |
50 | }
51 |
52 | extension DelegatedHTTPOperationTests: HTTPOperationDataDelegate {
53 |
54 | func delegatedHTTPOperation(_ operation: DelegatedHTTPOperation, didReceiveResponse response: HTTPURLResponse) {
55 | // no op
56 | }
57 |
58 | func delegatedHTTPOperation(_ operation: DelegatedHTTPOperation, didReceiveData data: Data) {
59 | XCTAssertEqual(OperationQueue.current, delegateQueue)
60 | receivedBytes += data.count
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/Sodes/SodesFoundationTests/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 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | BNDL
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Sodes/SwiftableFileHandle/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 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | NSPrincipalClass
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Sodes/SwiftableFileHandle/SODSwiftableFileHandle.h:
--------------------------------------------------------------------------------
1 | //
2 | // SODSwiftableFileHandle.h
3 | // SwiftableFileHandle
4 | //
5 | // Created by Jared Sinclair on 8/6/16.
6 | //
7 | //
8 |
9 | @import Foundation;
10 |
11 | NS_ASSUME_NONNULL_BEGIN
12 |
13 | @interface SODSwiftableFileHandle : NSObject
14 |
15 | - (nullable instancetype)initWithUrl:(NSURL *)fileUrl;
16 |
17 | - (BOOL)writeData:(NSData *)data at:(unsigned long long)location error:(NSError **)error;
18 |
19 | - (nullable NSData *)readDataFromLocation:(unsigned long long)location length:(unsigned long long)length error:(NSError **)error;
20 |
21 | - (void)synchronizeFile;
22 | - (void)closeFile;
23 |
24 | @end
25 |
26 | NS_ASSUME_NONNULL_END
27 |
--------------------------------------------------------------------------------
/Sodes/SwiftableFileHandle/SODSwiftableFileHandle.m:
--------------------------------------------------------------------------------
1 | //
2 | // SODSwiftableFileHandle.m
3 | // SwiftableFileHandle
4 | //
5 | // Created by Jared Sinclair on 8/6/16.
6 | //
7 | //
8 |
9 | #import "SODSwiftableFileHandle.h"
10 |
11 | @interface SODSwiftableFileHandle()
12 |
13 | @property (nonatomic, assign) BOOL isClosed;
14 | @property (nonatomic, readonly) NSFileHandle *handle;
15 | @property (nonatomic, readonly) NSLock *lock;
16 |
17 | @end
18 |
19 | @implementation SODSwiftableFileHandle
20 |
21 | @synthesize isClosed = _isClosed;
22 |
23 | - (nullable instancetype)initWithUrl:(NSURL *)fileUrl {
24 | NSError *error = nil;
25 | NSFileHandle *handle = [NSFileHandle fileHandleForUpdatingURL:fileUrl error:&error];
26 | if (error != nil || handle == nil) {
27 | NSLog(@"[SODSwiftableFileHandle initWithUrl:]: Unable to initialize because of error: %@", error);
28 | return nil;
29 | }
30 | self = [super init];
31 | if (self) {
32 | _handle = handle;
33 | _lock = [NSLock new];
34 | }
35 | return self;
36 | }
37 |
38 | - (BOOL)writeData:(NSData *)data at:(unsigned long long)location error:( NSError * _Nullable *)error {
39 |
40 | if (self.isClosed) { return NO; }
41 |
42 | BOOL successful;
43 | [self.lock lock];
44 | @try {
45 | [self.handle seekToFileOffset:location];
46 | [self.handle writeData:data];
47 | [self.handle synchronizeFile];
48 | successful = YES;
49 | } @catch (NSException *exception) {
50 | *error = [NSError errorWithDomain:exception.name code:10001 userInfo:exception.userInfo];
51 | successful = NO;
52 | } @finally {
53 | // no op
54 | }
55 | [self.lock unlock];
56 |
57 | return successful;
58 | }
59 |
60 | - (nullable NSData *)readDataFromLocation:(unsigned long long)location length:(unsigned long long)length error:(NSError * _Nullable __autoreleasing *)error {
61 |
62 | if (self.isClosed) { return nil; }
63 |
64 | NSData *data = nil;
65 | [self.lock lock];
66 | @try {
67 | [self.handle seekToFileOffset:location];
68 | NSData *readData = [self.handle readDataOfLength:(NSUInteger)length];
69 | if (readData.length == length) {
70 | data = readData;
71 | }
72 | } @catch (NSException *exception) {
73 | *error = [NSError errorWithDomain:exception.name code:10001 userInfo:exception.userInfo];
74 | } @finally {
75 | // no op
76 | }
77 | [self.lock unlock];
78 |
79 | return data;
80 | }
81 |
82 | - (void)synchronizeFile {
83 | if (self.isClosed) { return; }
84 | @try {
85 | [self.handle synchronizeFile];
86 | } @catch (NSException *exception) {
87 | NSLog(@"SODSwiftableFileHandle.synchronizeFile: %@", exception);
88 | } @finally {
89 | //
90 | }
91 | }
92 |
93 | - (void)closeFile {
94 | if (self.isClosed) { return; }
95 | @try {
96 | [self.handle closeFile];
97 | } @catch (NSException *exception) {
98 | NSLog(@"SODSwiftableFileHandle.closeFile: %@", exception);
99 | } @finally {
100 | //
101 | }
102 | }
103 |
104 | - (BOOL)isClosed {
105 | BOOL value;
106 | [self.lock lock];
107 | value = _isClosed;
108 | [self.lock unlock];
109 | return value;
110 | }
111 |
112 | - (void)setIsClosed:(BOOL)isClosed {
113 | [self.lock lock];
114 | _isClosed = isClosed;
115 | [self.lock unlock];
116 | }
117 |
118 | @end
119 |
--------------------------------------------------------------------------------
/Sodes/SwiftableFileHandle/SwiftableFileHandle.h:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftableFileHandle.h
3 | // SwiftableFileHandle
4 | //
5 | // Created by Jared Sinclair on 8/6/16.
6 | //
7 | //
8 |
9 | #import
10 |
11 | //! Project version number for SwiftableFileHandle.
12 | FOUNDATION_EXPORT double SwiftableFileHandleVersionNumber;
13 |
14 | //! Project version string for SwiftableFileHandle.
15 | FOUNDATION_EXPORT const unsigned char SwiftableFileHandleVersionString[];
16 |
17 | // In this header, you should import all the public headers of your framework using statements like #import
18 |
19 | #import
20 |
21 |
22 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaredsinclair/sodes-audio-example/72548e948d767ba0b3c2894c13b664c843fbd9a6/screenshot.png
--------------------------------------------------------------------------------