├── .gitignore
├── .idea
├── FreePlayer.iml
├── modules.xml
├── runConfigurations
│ ├── FreePlayer.xml
│ └── FreePlayer_macOS.xml
├── vcs.xml
├── workspace.xml
└── xcode.xml
├── FPDemo
├── AppDelegate.swift
├── Assets.xcassets
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
├── Info.plist
├── ViewController.swift
├── id3v1.1.mp3
├── id3v2.3.mp3
├── id3v2.4.mp3
├── 久远-光と波の记忆.mp3
└── 红莲の弓矢.wav
├── FreePlayer.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ └── contents.xcworkspacedata
└── xcshareddata
│ └── xcschemes
│ ├── FreePlayer macOS.xcscheme
│ └── FreePlayer.xcscheme
├── FreePlayer
├── FreePlayer.h
└── Info.plist
├── FreePlayerOSXDemo
├── AppDelegate.swift
├── Assets.xcassets
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── Base.lproj
│ └── Main.storyboard
├── FreePlayerOSXDemo.entitlements
├── Info.plist
└── ViewController.swift
├── FreePlayerTests
├── FreePlayerTests.swift
└── Info.plist
├── LICENSE
├── README.MD
└── Sources
├── AUPlayer.swift
├── AudioQueue.swift
├── AudioStream.swift
├── CachingStream.swift
├── DisplayLink.swift
├── Extension.swift
├── FileStream.swift
├── FreePlayer+MPPlayingCenter.swift
├── FreePlayer.swift
├── Global.swift
├── HttpStream.swift
├── ID3Parser.swift
├── Log.swift
├── LoggerVC.swift
├── Reachability.swift
├── RunloopQueue.swift
├── StreamConfiguration.swift
├── StreamInput.swift
├── StreamOutput.swift
└── StreamProvider.swift
/.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 | project_backup/
9 | .DS_Store
10 |
11 | ## Various settings
12 | *.pbxuser
13 | !default.pbxuser
14 | *.mode1v3
15 | !default.mode1v3
16 | *.mode2v3
17 | !default.mode2v3
18 | *.perspectivev3
19 | !default.perspectivev3
20 | xcuserdata/
21 |
22 | ## Other
23 | *.moved-aside
24 | *.xcuserstate
25 |
26 | ## Obj-C/Swift specific
27 | *.hmap
28 | *.ipa
29 | *.dSYM.zip
30 | *.dSYM
31 |
32 | ## Playgrounds
33 | timeline.xctimeline
34 | playground.xcworkspace
35 |
36 | # Swift Package Manager
37 | #
38 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
39 | # Packages/
40 | .build/
41 |
42 | # CocoaPods
43 | #
44 | # We recommend against adding the Pods directory to your .gitignore. However
45 | # you should judge for yourself, the pros and cons are mentioned at:
46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
47 | #
48 | # Pods/
49 |
50 | # Carthage
51 | #
52 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
53 | Carthage/Checkouts
54 | Carthage/Build
55 |
56 | # fastlane
57 | #
58 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
59 | # screenshots whenever they are needed.
60 | # For more information about the recommended setup visit:
61 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
62 |
63 | fastlane/report.xml
64 | fastlane/Preview.html
65 | fastlane/screenshots
66 | fastlane/test_output
67 | # fastlane stuff
68 | *.ipa
69 | *.mobileprovision
70 | *.dSYM.zip
71 | *.cer
72 | *.certSigningRequest
73 | *.p12
74 | upload_app_to_bugly_result.json
75 | update_app_to_bugly_result.json
76 |
--------------------------------------------------------------------------------
/.idea/FreePlayer.iml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/FreePlayer.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/FreePlayer_macOS.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
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 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | true
47 | DEFINITION_ORDER
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
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 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 | 1505813008327
126 |
127 |
128 | 1505813008327
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
--------------------------------------------------------------------------------
/.idea/xcode.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/FPDemo/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // FPDemo
4 | //
5 | // Created by Lincoln Law on 2017/2/21.
6 | // Copyright © 2017年 Lincoln Law. All rights reserved.
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 |
48 |
--------------------------------------------------------------------------------
/FPDemo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/FPDemo/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 |
--------------------------------------------------------------------------------
/FPDemo/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 |
--------------------------------------------------------------------------------
/FPDemo/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 | UIBackgroundModes
29 |
30 | audio
31 | fetch
32 |
33 | UILaunchStoryboardName
34 | LaunchScreen
35 | UIMainStoryboardFile
36 | Main
37 | UIRequiredDeviceCapabilities
38 |
39 | armv7
40 |
41 | UISupportedInterfaceOrientations
42 |
43 | UIInterfaceOrientationPortrait
44 | UIInterfaceOrientationLandscapeLeft
45 | UIInterfaceOrientationLandscapeRight
46 |
47 | UISupportedInterfaceOrientations~ipad
48 |
49 | UIInterfaceOrientationPortrait
50 | UIInterfaceOrientationPortraitUpsideDown
51 | UIInterfaceOrientationLandscapeLeft
52 | UIInterfaceOrientationLandscapeRight
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/FPDemo/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // FPDemo
4 | //
5 | // Created by Lincoln Law on 2017/2/21.
6 | // Copyright © 2017年 Lincoln Law. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import FreePlayer
11 | class ViewController: UIViewController {
12 | private var _localMP3: URL?
13 | private var _player: FreePlayer?
14 |
15 | private var _butttonReset = UIButton(frame: CGRect(x: 100, y: 100, width: 100, height: 50))
16 | private var _butttonClear = UIButton(frame: CGRect(x: 100, y: 200, width: 100, height: 50))
17 | private var _butttonSeek = UIButton(frame: CGRect(x: 100, y: 300, width: 100, height: 50))
18 |
19 | private var _slider = UISlider(frame: CGRect(x: 100, y: 360, width: 200, height: 20))
20 |
21 | override func viewDidLoad() {
22 | super.viewDidLoad()
23 | StreamConfiguration.shared.cacheEnabled = false
24 | StreamConfiguration.shared.autoFillID3InfoToNowPlayingCenter = true
25 | view.addSubview(_butttonReset)
26 | view.addSubview(_butttonClear)
27 | view.addSubview(_butttonSeek)
28 | view.addSubview(_slider)
29 |
30 | _butttonReset.backgroundColor = UIColor.orange
31 | _butttonClear.backgroundColor = UIColor.orange
32 | _butttonSeek.backgroundColor = UIColor.orange
33 |
34 | _butttonReset.setTitle("Reset", for: .normal)
35 | _butttonClear.setTitle("Clean", for: .normal)
36 | _butttonSeek.setTitle("Seek", for: .normal)
37 |
38 | _butttonReset.addTarget(self, action: #selector(ViewController.reset), for: .touchUpInside)
39 | _butttonClear.addTarget(self, action: #selector(ViewController.clean), for: .touchUpInside)
40 | _butttonSeek.addTarget(self, action: #selector(ViewController.seek), for: .touchUpInside)
41 | _slider.addTarget(self, action: #selector(ViewController.valueChange), for: .valueChanged)
42 |
43 | // FPLogger.enable(modules: [FPLogger.Module.httpStream])
44 | _slider.maximumValue = 1
45 | _slider.minimumValue = 0
46 | // FPLogger.shared.logToFile = false
47 | // StreamConfiguration.shared.requireNetworkChecking = false
48 | // print(StreamConfiguration.shared)
49 | _localMP3 = Bundle.main.url(forResource: "久远-光と波の记忆", withExtension: "mp3")
50 | //"http://mp3-cdn.luoo.net/low/luoo/radio895/03.mp3"
51 | // "http://199.180.75.58:9061/stream"
52 |
53 | // _localMP3 = URL(string: "http://mp3-cdn.luoo.net/low/package/neoclassic01/radio01/03.mp3")
54 | // _localMP3 = URL(string: "http://www.kozco.com/tech/32.mp3")
55 | // _localMP3 = URL(string: "http://199.180.75.58:9061/stream")
56 |
57 | // Do any additional setup after loading the view, typically from a nib.
58 | }
59 |
60 | override func viewDidAppear(_ animated: Bool) {
61 | super.viewDidAppear(animated)
62 | UIApplication.shared.beginReceivingRemoteControlEvents()
63 | }
64 | @objc private func valueChange() {
65 | _player?.volume = _slider.value
66 | }
67 |
68 | @objc private func reset() {
69 | if _player == nil {
70 | _player = FreePlayer()
71 | _player?.networkPermisionHandler = { done in
72 | done(true)
73 | }
74 | }
75 | _player?.play(from: _localMP3)
76 | NowPlayingInfo.shared.image(with: "http://img-cdn.luoo.net/pics/vol/58a9c994116ae.jpg")
77 | }
78 |
79 | @objc private func seek() {
80 | _player?.seek(to: 30)
81 | }
82 |
83 | @objc private func clean() {
84 | _player = nil
85 | }
86 |
87 | override func didReceiveMemoryWarning() {
88 | super.didReceiveMemoryWarning()
89 | // Dispose of any resources that can be recreated.
90 | }
91 |
92 | open override var canBecomeFirstResponder: Bool { return true }
93 | open override func remoteControlReceived(with event: UIEvent?) {
94 | guard let value = event?.subtype else { return }
95 | let manager = _player
96 | let playing = _player?.isPlaying ?? false
97 | switch value {
98 | case .remoteControlPause: manager?.pause()
99 | case .remoteControlPlay: manager?.resume()
100 | case .remoteControlTogglePlayPause: playing ? manager?.pause() : manager?.resume()
101 | case .remoteControlNextTrack, .remoteControlBeginSeekingForward: break//manager.next()
102 | case .remoteControlPreviousTrack, .remoteControlBeginSeekingBackward: break//manager.previous()
103 | default: break
104 | }
105 | }
106 |
107 |
108 | }
109 |
110 |
--------------------------------------------------------------------------------
/FPDemo/id3v1.1.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeEagle/FreePlayer/89ef88ed3efbc31433608a917ee1671b014116b9/FPDemo/id3v1.1.mp3
--------------------------------------------------------------------------------
/FPDemo/id3v2.3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeEagle/FreePlayer/89ef88ed3efbc31433608a917ee1671b014116b9/FPDemo/id3v2.3.mp3
--------------------------------------------------------------------------------
/FPDemo/id3v2.4.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeEagle/FreePlayer/89ef88ed3efbc31433608a917ee1671b014116b9/FPDemo/id3v2.4.mp3
--------------------------------------------------------------------------------
/FPDemo/久远-光と波の记忆.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeEagle/FreePlayer/89ef88ed3efbc31433608a917ee1671b014116b9/FPDemo/久远-光と波の记忆.mp3
--------------------------------------------------------------------------------
/FPDemo/红莲の弓矢.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CodeEagle/FreePlayer/89ef88ed3efbc31433608a917ee1671b014116b9/FPDemo/红莲の弓矢.wav
--------------------------------------------------------------------------------
/FreePlayer.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/FreePlayer.xcodeproj/xcshareddata/xcschemes/FreePlayer macOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
33 |
34 |
35 |
36 |
47 |
48 |
54 |
55 |
56 |
57 |
58 |
59 |
65 |
66 |
72 |
73 |
74 |
75 |
77 |
78 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/FreePlayer.xcodeproj/xcshareddata/xcschemes/FreePlayer.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
31 |
32 |
34 |
40 |
41 |
42 |
43 |
44 |
50 |
51 |
52 |
53 |
54 |
55 |
66 |
67 |
73 |
74 |
75 |
76 |
77 |
78 |
84 |
85 |
91 |
92 |
93 |
94 |
96 |
97 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/FreePlayer/FreePlayer.h:
--------------------------------------------------------------------------------
1 | //
2 | // FreePlayer.h
3 | // FreePlayer
4 | //
5 | // Created by Lincoln Law on 2017/2/19.
6 | // Copyright © 2017年 Lincoln Law. All rights reserved.
7 | //
8 |
9 | @import Foundation;
10 | //! Project version number for FreePlayer.
11 | FOUNDATION_EXPORT double FreePlayerVersionNumber;
12 |
13 | //! Project version string for FreePlayer.
14 | FOUNDATION_EXPORT const unsigned char FreePlayerVersionString[];
15 |
16 | // In this header, you should import all the public headers of your framework using statements like
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/FreePlayer/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.1
19 | CFBundleVersion
20 | 1
21 | NSPrincipalClass
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/FreePlayerOSXDemo/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // FreePlayerOSXDemo
4 | //
5 | // Created by lincolnlaw on 2017/7/27.
6 | // Copyright © 2017年 Lincoln Law. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 |
11 | @NSApplicationMain
12 | class AppDelegate: NSObject, NSApplicationDelegate {
13 |
14 |
15 |
16 | func applicationDidFinishLaunching(_ aNotification: Notification) {
17 | // Insert code here to initialize your application
18 | }
19 |
20 | func applicationWillTerminate(_ aNotification: Notification) {
21 | // Insert code here to tear down your application
22 | }
23 |
24 |
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/FreePlayerOSXDemo/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "mac",
5 | "size" : "16x16",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "size" : "16x16",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "size" : "32x32",
16 | "scale" : "1x"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "size" : "32x32",
21 | "scale" : "2x"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "size" : "128x128",
26 | "scale" : "1x"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "size" : "128x128",
31 | "scale" : "2x"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "size" : "256x256",
36 | "scale" : "1x"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "size" : "256x256",
41 | "scale" : "2x"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "size" : "512x512",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "size" : "512x512",
51 | "scale" : "2x"
52 | }
53 | ],
54 | "info" : {
55 | "version" : 1,
56 | "author" : "xcode"
57 | }
58 | }
--------------------------------------------------------------------------------
/FreePlayerOSXDemo/FreePlayerOSXDemo.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/FreePlayerOSXDemo/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIconFile
10 |
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | APPL
19 | CFBundleShortVersionString
20 | 1.0
21 | CFBundleVersion
22 | 1
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSAppTransportSecurity
26 |
27 | NSAllowsArbitraryLoads
28 |
29 |
30 | NSHumanReadableCopyright
31 | Copyright © 2017年 Lincoln Law. All rights reserved.
32 | NSMainStoryboardFile
33 | Main
34 | NSPrincipalClass
35 | NSApplication
36 |
37 |
38 |
--------------------------------------------------------------------------------
/FreePlayerOSXDemo/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // FreePlayerOSXDemo
4 | //
5 | // Created by lincolnlaw on 2017/7/27.
6 | // Copyright © 2017年 Lincoln Law. All rights reserved.
7 | //
8 |
9 | import Cocoa
10 | import FreePlayer
11 | class ViewController: NSViewController {
12 |
13 | private var _localMP3: URL?
14 | private var _player = FreePlayer()
15 |
16 | override func viewDidLoad() {
17 | super.viewDidLoad()
18 | // _localMP3 = URL(string: "http://mp3-cdn2.luoo.net/low/package/vinyl01/radio32/06.mp3")!
19 | // _localMP3 = Bundle.main.url(forResource: "久远-光と波の记忆", withExtension: "mp3")
20 | _localMP3 = URL(string: "http://mp3-cdn.luoo.net/low/package/neoclassic01/radio01/03.mp3")
21 | // _localMP3 = URL(string: "http://87.98.216.129:4240/;?icy=http")
22 | // _localMP3 = URL(string: "http://94.23.148.11:8392/stream?icy=http")
23 | _player.play(from: _localMP3)
24 | // Do any additional setup after loading the view.
25 | }
26 |
27 | override var representedObject: Any? {
28 | didSet {
29 | // Update the view, if already loaded.
30 | }
31 | }
32 |
33 |
34 | }
35 |
36 |
--------------------------------------------------------------------------------
/FreePlayerTests/FreePlayerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FreePlayerTests.swift
3 | // FreePlayerTests
4 | //
5 | // Created by Lincoln Law on 2017/2/19.
6 | // Copyright © 2017年 Lincoln Law. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import FreePlayer
11 |
12 | class FreePlayerTests: XCTestCase {
13 |
14 | fileprivate var data: AudioStream?
15 |
16 | override func setUp() {
17 | super.setUp()
18 | // Put setup code here. This method is called before the invocation of each test method in the class.
19 | }
20 |
21 | override func tearDown() {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | super.tearDown()
24 | }
25 |
26 | func testHttpStream() {
27 | let hs = HttpStream()
28 | _ = hs.createReadStream(from: URL(string: "http://mp3-cdn.luoo.net/low/luoo/radio896/02.mp3"))
29 | }
30 |
31 | func testPerformanceExample() {
32 | // This is an example of a performance test case.
33 | self.measure {
34 | // Put the code you want to measure the time of here.
35 | }
36 | }
37 |
38 | func asyncTest(timeout: TimeInterval = 30, block: (XCTestExpectation) -> ()) {
39 | let expectation: XCTestExpectation = self.expectation(description: "❌:Timeout")
40 | block(expectation)
41 | self.waitForExpectations(timeout: timeout) { (error) in
42 | if let err = error {
43 | XCTFail("time out: \(err)")
44 | } else {
45 | XCTAssert(true, "success")
46 | }
47 | }
48 | }
49 |
50 | }
51 |
52 | extension FreePlayerTests {
53 |
54 | func testOutPut() {
55 | let path = Bundle(for: FreePlayerTests.self).bundlePath.replacingOccurrences(of: "FreePlayerTests.xctest", with: "out.txt")
56 | let output = StreamOutputManager(fileURL: URL(fileURLWithPath: path))
57 | guard let data = "hello world😄".data(using: .utf8) else { return }
58 | data.withUnsafeBytes { (d: UnsafePointer) -> Void in
59 | output.write(data: d, length: data.count)
60 | }
61 | guard let data2 = "hello world😄2222".data(using: .utf8) else { return }
62 | data2.withUnsafeBytes { (d: UnsafePointer) -> Void in
63 | output.write(data: d, length: data2.count)
64 | }
65 | }
66 |
67 | func testConfiguration() {
68 | // _ = StreamConfiguration.shared
69 | // let queue = AudioQueue()
70 | // queue.start()
71 | // queue.stop()
72 | asyncTest { (e) in
73 | data = AudioStream()
74 | DispatchQueue.main.asyncAfter(deadline: .now() + 10, execute: {
75 | e.fulfill()
76 | })
77 | }
78 |
79 |
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/FreePlayerTests/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 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011-2016 Matias Muhonen 穆马帝
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions
6 | are met:
7 | 1. Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | 2. Redistributions in binary form must reproduce the above copyright
10 | notice, this list of conditions and the following disclaimer in the
11 | documentation and/or other materials provided with the distribution.
12 | 3. The name of the author may not be used to endorse or promote products
13 | derived from this software without specific prior written permission.
14 |
15 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
16 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
17 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
18 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
19 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
20 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
24 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 |
26 |
27 | The FreeStreamer framework bundles Reachability which is licensed under the following
28 | license:
29 |
30 | Copyright (c) 2011, Tony Million.
31 | All rights reserved.
32 |
33 | Redistribution and use in source and binary forms, with or without
34 | modification, are permitted provided that the following conditions are met:
35 |
36 | 1. Redistributions of source code must retain the above copyright notice, this
37 | list of conditions and the following disclaimer.
38 |
39 | 2. Redistributions in binary form must reproduce the above copyright notice,
40 | this list of conditions and the following disclaimer in the documentation
41 | and/or other materials provided with the distribution.
42 |
43 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
44 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
45 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
46 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
47 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
48 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
49 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
50 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
51 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
52 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
53 | POSSIBILITY OF SUCH DAMAGE.
54 |
--------------------------------------------------------------------------------
/README.MD:
--------------------------------------------------------------------------------
1 |
2 | ⚠️Deprecated⚠️
3 | ---
4 | Use [APlay](https://github.com/CodeEagle/APlay) instead.
5 |
6 | FreePlayer
7 | ---
8 | Swift framework for streaming, playing audio
9 | >The project is based on the project [FreeStreamer](https://github.com/muhku/FreeStreamer)
10 |
11 | why?
12 | ---
13 | - Pure Swift implementation, no need to mixing Objective-C/C++ in project
14 |
15 | Features
16 | ---
17 | - [x] Digest(Tested), Basic(not tested) proxy support
18 |
19 | - [x] CPU-friendly design(4% - 6%)
20 |
21 | - [x] Multiple protocols supported: ShoutCast, standard HTTP, local files
22 |
23 | - [x] Prepared for tough network conditions: adjustable buffer sizes, stream pre-buffering and restart on failures,restart on not full content streamed when end of stream
24 |
25 | - [x] Metadata support: ShoutCast metadata, ID3V1, ID3v1.1, ID3v2.2, ID3v2.3, ID3v2.4
26 |
27 | - [x] Local disk caching: user only needs to stream a file once and after that it can be played from a local cache
28 |
29 | - [x] Local disk storing: user can add a folder for local resource loading
30 |
31 | - [x] Preloading: playback can start immediately without needing to wait for buffering
32 |
33 | - [x] Record: support recording the stream contents to a file
34 |
35 | - [ ] Access the PCM audio samples: as an example, a visualizer is included
36 |
37 | - [x] custom logging module and logging into file supported
38 |
39 | - [ ] FLAC support
40 |
41 | License
42 | ---
43 | See [LICENSE](https://github.com/CodeEagle/FreePlayer/blob/master/LICENSE) for the license.
44 |
--------------------------------------------------------------------------------
/Sources/AUPlayer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AUPlayer.swift
3 | // VinylPlayer
4 | //
5 | // Created by lincolnlaw on 2017/9/1.
6 | // Copyright © 2017年 lincolnlaw. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import AudioUnit
11 | #if os(iOS)
12 | import UIKit
13 | import AVFoundation
14 | #endif
15 |
16 | public final class AUPlayer {
17 |
18 | private var _provider: StreamProvider?
19 | private lazy var _stream: AudioStream? = nil//AudioStream()
20 |
21 | private lazy var _converterNodes: [AUNode] = []
22 | private lazy var _volume: Float = 1
23 |
24 | private var _audioGraph: AUGraph?
25 |
26 | private var _equalizerEnabled = false
27 | private var _equalizerOn = false
28 |
29 | private var usedSize: Int = 0
30 |
31 | private var maxSizeForNextRead = 0
32 | private var _cachedSize = 0
33 |
34 | private lazy var _eqNode: AUNode = 0
35 | private lazy var _mixerNode: AUNode = 0
36 | private lazy var _outputNode: AUNode = 0
37 |
38 | private lazy var _eqInputNode: AUNode = 0
39 | private lazy var _eqOutputNode: AUNode = 0
40 | private lazy var _mixerInputNode: AUNode = 0
41 | private lazy var _mixerOutputNode: AUNode = 0
42 |
43 | private var _eqUnit: AudioComponentInstance?
44 | private var _mixerUnit: AudioComponentInstance?
45 | private var _outputUnit: AudioComponentInstance?
46 |
47 | private var _audioConverterRef: AudioConverterRef?
48 | private var _audioConverterAudioStreamBasicDescription: AudioStreamBasicDescription = AudioStreamBasicDescription()
49 |
50 | private lazy var _eqBandCount: UInt32 = 0
51 |
52 | private var state: State {
53 | get { return _stateQueue.sync { return _state } }
54 | set {
55 | _stateQueue.sync { _state = newValue }
56 | _stream?.audioQueueStateChanged(state: newValue)
57 | }
58 | }
59 | private var _state: State = .idle
60 | private lazy var _seekToTimeWasRequested: Bool = false
61 |
62 | private lazy var _stateQueue = DispatchQueue(label: "VinylPlayer.AUPlayer.stateQueue")
63 |
64 |
65 | private let _pcmBufferFrameSizeInBytes: UInt32 = AUPlayer.canonical.mBytesPerFrame
66 |
67 | private var _progress: Double = 0
68 |
69 | #if os(iOS)
70 | private lazy var _backgroundTask = UIBackgroundTaskInvalid
71 | #endif
72 |
73 | private lazy var _currentIndex = 0
74 | private lazy var _pageSize = AUPlayer.maxReadPerSlice
75 | private func increaseBufferIndex() {
76 | _stateQueue.sync {
77 | _currentIndex = (_currentIndex + 1) % Int(AUPlayer.minimumBufferCount)
78 | }
79 | }
80 | private lazy var _buffers: UnsafeMutablePointer = {
81 | let size = AUPlayer.minimumBufferSize
82 | let b = malloc(size).assumingMemoryBound(to: UInt8.self)
83 | b.initialize(to: 0, count: size)
84 | return b
85 | }()
86 |
87 | deinit {
88 | let size = AUPlayer.minimumBufferSize
89 | _buffers.deinitialize(count: size)
90 | _buffers.deallocate(capacity: size)
91 | }
92 |
93 | public init() {
94 | createAudioGraph()
95 | }
96 |
97 | public func play(url: URL) {
98 | _provider = StreamProvider(url: url)
99 | _provider?.autoProduce = false
100 | _provider?.open()
101 | resume()
102 | }
103 | }
104 |
105 |
106 | // MARK: - Open API
107 | extension AUPlayer {
108 |
109 | func pause() {
110 | guard let audioGraph = _audioGraph else { return }
111 | if AUGraphStop(audioGraph) != noErr {
112 | _stream?.audioQueueInitializationFailed()
113 | return
114 | }
115 | state = .paused
116 | #if os(iOS)
117 | NowPlayingInfo.shared.pause(elapsedPlayback: currentTime())
118 | #endif
119 | }
120 |
121 | func resume() {
122 |
123 | guard let graph = _audioGraph else { return }
124 | if state == .paused {
125 | state = .running
126 | guard let audioGraph = _audioGraph else { return }
127 | if AUGraphStart(audioGraph) != noErr {
128 | _stream?.audioQueueInitializationFailed()
129 | return
130 | }
131 | } else if state == .idle {
132 | _progress = 0
133 | if audioGraphIsRunning() { return }
134 | do {
135 | try AUGraphStart(graph).throwCheck()
136 | startBackgroundTask()
137 | state = .running
138 | } catch {
139 |
140 | }
141 | }
142 | #if os(iOS)
143 | NowPlayingInfo.shared.play(elapsedPlayback: currentTime())
144 | #endif
145 |
146 | }
147 |
148 | func currentTime() -> Double {
149 | return _progress
150 | }
151 |
152 | func togglePlayPause() {
153 | _state == .paused ? resume() : pause()
154 | }
155 |
156 | func next() { }
157 |
158 | func previous() { }
159 |
160 | func seek(to time: Float) { }
161 |
162 | func setVolume(value: Float) {
163 | _volume = value
164 | #if os(iOS)
165 | if let unit = _mixerUnit {
166 | AudioUnitSetParameter(unit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Output, 0, value, 0)
167 | }
168 | #else
169 | if let unit = _mixerUnit {
170 | AudioUnitSetParameter(unit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Output, 0, value, 0)
171 | } else if let unit = _outputUnit {
172 | AudioUnitSetParameter(unit, kHALOutputParam_Volume, kAudioUnitScope_Output, AUPlayer.Bus.output, value, 0)
173 | }
174 | #endif
175 | }
176 | }
177 |
178 | // MARK: - Create
179 | private extension AUPlayer {
180 | // MARK: createAudioGraph
181 | func createAudioGraph() {
182 | do {
183 | try NewAUGraph(&_audioGraph).throwCheck()
184 | guard let graph = _audioGraph else { return }
185 | try AUGraphOpen(graph).throwCheck()
186 | createEqUnit()
187 | createMixerUnit()
188 | createOutputUnit()
189 | connectGraph()
190 | try AUGraphInitialize(graph).throwCheck()
191 | setVolume(value: 1)
192 | } catch {
193 | _stream?.audioQueueInitializationFailed()
194 | }
195 | }
196 |
197 |
198 | private func createEqUnit() {
199 | #if os(OSX)
200 | guard #available(OSX 10.9, *) else { return }
201 | #endif
202 | let _options = StreamConfiguration.shared
203 | guard let value = _options.equalizerBandFrequencies[fp_safe: 0], value != 0, let audioGraph = _audioGraph else { return }
204 | do {
205 | try AUGraphAddNode(audioGraph, &AUPlayer.nbandUnit, &_eqNode).throwCheck()
206 | try AUGraphNodeInfo(audioGraph, _eqNode, nil, &_eqUnit).throwCheck()
207 | guard let eqUnit = _eqUnit else { return }
208 | let size = MemoryLayout.size(ofValue: AUPlayer.maxFramesPerSlice)
209 | try AudioUnitSetProperty(eqUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &AUPlayer.maxFramesPerSlice, UInt32(size)).throwCheck()
210 | while _options.equalizerBandFrequencies[fp_safe: Int(_eqBandCount)] != nil {
211 | _eqBandCount += 1
212 | }
213 | let eqBandSize = UInt32(MemoryLayout.size(ofValue: _eqBandCount))
214 | try AudioUnitSetProperty(eqUnit, kAUNBandEQProperty_NumberOfBands, kAudioUnitScope_Global, 0, &_eqBandCount, eqBandSize).throwCheck()
215 | for i in 0..<_eqBandCount {
216 | let value = _options.equalizerBandFrequencies[Int(i)]
217 | try AudioUnitSetParameter(eqUnit, kAUNBandEQParam_Frequency + i, kAudioUnitScope_Global, 0, value, 0).throwCheck()
218 | }
219 |
220 | for i in 0..<_eqBandCount {
221 | try AudioUnitSetParameter(eqUnit, kAUNBandEQParam_BypassBand + i, kAudioUnitScope_Global, 0, 0, 0).throwCheck()
222 | }
223 | } catch { _stream?.audioQueueInitializationFailed() }
224 | }
225 |
226 | private func createMixerUnit() {
227 | let _options = StreamConfiguration.shared
228 | guard _options.enableVolumeMixer, let graph = _audioGraph else { return }
229 | do {
230 | try AUGraphAddNode(graph, &AUPlayer.mixer, &_mixerNode).throwCheck()
231 | try AUGraphNodeInfo(graph, _mixerNode, &AUPlayer.mixer, &_mixerUnit).throwCheck()
232 | guard let mixerUnit = _mixerUnit else { return }
233 | let size = UInt32(MemoryLayout.size(ofValue: AUPlayer.maxFramesPerSlice))
234 | try AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &AUPlayer.maxFramesPerSlice, size).throwCheck()
235 | var busCount: UInt32 = 1
236 | let busCountSize = UInt32(MemoryLayout.size(ofValue: busCount))
237 | try AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_ElementCount, kAudioUnitScope_Input, 0, &busCount, busCountSize).throwCheck()
238 | var graphSampleRate: Float64 = 44100
239 | let graphSampleRateSize = UInt32(MemoryLayout.size(ofValue: graphSampleRate))
240 | try AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_SampleRate, kAudioUnitScope_Output, 0, &graphSampleRate, graphSampleRateSize).throwCheck()
241 | try AudioUnitSetParameter(mixerUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, 0, 1, 0).throwCheck()
242 | } catch { _stream?.audioQueueInitializationFailed() }
243 | }
244 |
245 | private func createOutputUnit() {
246 | guard let audioGraph = _audioGraph else { return }
247 | do {
248 | try AUGraphAddNode(audioGraph, &AUPlayer.outputUnit, &_outputNode).throwCheck()
249 | try AUGraphNodeInfo(audioGraph, _outputNode, &AUPlayer.outputUnit, &_outputUnit).throwCheck()
250 | guard let unit = _outputUnit else { return }
251 | #if !os(OSX)
252 | var flag: UInt32 = 1
253 | let size = UInt32(MemoryLayout.size(ofValue: flag))
254 | try AudioUnitSetProperty(unit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, AUPlayer.Bus.output, &flag, size).throwCheck()
255 | flag = 0
256 | try AudioUnitSetProperty(unit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, AUPlayer.Bus.input, &flag, size).throwCheck()
257 | #endif
258 | let s = MemoryLayout.size(ofValue: AUPlayer.canonical)
259 | try AudioUnitSetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, AUPlayer.Bus.output, &AUPlayer.canonical, UInt32(s)).throwCheck()
260 | } catch { _stream?.audioQueueInitializationFailed() }
261 | }
262 |
263 | private func connectGraph() {
264 | guard let audioGraph = _audioGraph else { return }
265 | AUGraphClearConnections(audioGraph)
266 | for node in _converterNodes {
267 | AUGraphRemoveNode(audioGraph, node).check()
268 | }
269 | _converterNodes.removeAll()
270 | var nodes: [AUNode] = []
271 | var units: [AudioComponentInstance] = []
272 | if let unit = _eqUnit {
273 | if _equalizerEnabled {
274 | nodes.append(_eqNode)
275 | units.append(unit)
276 | _equalizerOn = true
277 | } else {
278 | _equalizerOn = false
279 | }
280 | } else {
281 | _equalizerOn = false
282 | }
283 |
284 | if let unit = _mixerUnit {
285 | nodes.append(_mixerNode)
286 | units.append(unit)
287 | }
288 |
289 | if let unit = _outputUnit {
290 | nodes.append(_outputNode)
291 | units.append(unit)
292 | }
293 | if let node = nodes.first, let unit = units.first {
294 | setOutputCallback(for: node, unit: unit)
295 | }
296 | for i in 0.. OSStatus in
308 | let sself = userInfo.to(object: AUPlayer.self)
309 | return sself.outputRenderCallback(ioActionFlags: ioActionFlags, inTimeStamp: inTimeStamp, inBusNumber: inBusNumber, inNumberFrames: inNumberFrames, ioData: ioBufferList)
310 | }
311 | let pointer = UnsafeMutableRawPointer.from(object: self)
312 | var callbackStruct = AURenderCallbackStruct(inputProc: callback, inputProcRefCon: pointer)
313 | status = AudioUnitSetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &AUPlayer.canonical, AUPlayer.canonicalSize)
314 | guard let audioGraph = _audioGraph else { return }
315 | do {
316 | if status == noErr {
317 | try AUGraphSetNodeInputCallback(audioGraph, node, 0, &callbackStruct).throwCheck()
318 | } else {
319 | var format: AudioStreamBasicDescription = AudioStreamBasicDescription()
320 | var size = UInt32(MemoryLayout.size)
321 | try AudioUnitGetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &format, &size).throwCheck()
322 | let converterNode = createConverterNode(for: AUPlayer.canonical, destFormat: format)
323 | guard let c = converterNode else { return }
324 | try AUGraphSetNodeInputCallback(audioGraph, c, 0, &callbackStruct).throwCheck()
325 | try AUGraphConnectNodeInput(audioGraph, c, 0, node, 0).throwCheck()
326 | }
327 | } catch { _stream?.audioQueueInitializationFailed() }
328 | }
329 |
330 | func connect(node: AUNode, destNode: AUNode, unit: AudioComponentInstance, destUnit: AudioComponentInstance) {
331 | guard let audioGraph = _audioGraph else { return }
332 | var status: OSStatus = noErr
333 | var needConverter = false
334 | var srcFormat: AudioStreamBasicDescription = AudioStreamBasicDescription()
335 | var desFormat: AudioStreamBasicDescription = AudioStreamBasicDescription()
336 | var size = UInt32(MemoryLayout.size)
337 | do {
338 | try AudioUnitGetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &srcFormat, &size).throwCheck()
339 | try AudioUnitGetProperty(destUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &desFormat, &size).throwCheck()
340 |
341 | needConverter = memcmp(&srcFormat, &desFormat, MemoryLayout.size(ofValue: srcFormat)) != 0
342 | if needConverter {
343 | status = AudioUnitSetProperty(destUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &srcFormat, UInt32(MemoryLayout.size(ofValue: srcFormat)))
344 | needConverter = status != noErr
345 | }
346 | if needConverter {
347 | if let convertNode = createConverterNode(for: srcFormat, destFormat: desFormat){
348 | try AUGraphConnectNodeInput(audioGraph, node, 0, convertNode, 0).throwCheck()
349 | try AUGraphConnectNodeInput(audioGraph, convertNode, 0, destNode, 0).throwCheck()
350 | }
351 |
352 | } else {
353 | try AUGraphConnectNodeInput(audioGraph, node, 0, destNode, 0).throwCheck()
354 | }
355 | } catch { _stream?.audioQueueInitializationFailed() }
356 | }
357 |
358 | func createConverterNode(for format: AudioStreamBasicDescription, destFormat: AudioStreamBasicDescription) -> AUNode? {
359 | guard let audioGraph = _audioGraph else { return nil }
360 | var convertNode = AUNode()
361 | var convertUnit: AudioComponentInstance?
362 | do {
363 | try AUGraphAddNode(audioGraph, &AUPlayer.convertUnit, &convertNode).throwCheck()
364 | try AUGraphNodeInfo(audioGraph, convertNode, &AUPlayer.mixer, &convertUnit).throwCheck()
365 | guard let unit = convertUnit else { return nil }
366 | var srcFormat = format
367 | try AudioUnitSetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &srcFormat, UInt32(MemoryLayout.size(ofValue: format))).throwCheck()
368 | var desFormat = destFormat
369 | try AudioUnitSetProperty(unit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &desFormat, UInt32(MemoryLayout.size(ofValue: destFormat))).throwCheck()
370 | try AudioUnitSetProperty(unit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &AUPlayer.maxFramesPerSlice, UInt32(MemoryLayout.size(ofValue: AUPlayer.maxFramesPerSlice))).throwCheck()
371 | _converterNodes.append(convertNode)
372 | return convertNode
373 | } catch {
374 | _stream?.audioQueueInitializationFailed()
375 | return nil
376 | }
377 | }
378 |
379 | private func audioGraphIsRunning() -> Bool {
380 | guard let graph = _audioGraph else { return false }
381 | var isRuning: DarwinBoolean = false
382 | guard AUGraphIsRunning(graph, &isRuning) == noErr else { return false }
383 | return isRuning.boolValue
384 | }
385 |
386 | @discardableResult private func startAudioGraph() -> Bool {
387 | guard let graph = _audioGraph else { return false }
388 | _progress = 0
389 | if audioGraphIsRunning() { return false }
390 | do {
391 | try AUGraphStart(graph).throwCheck()
392 | startBackgroundTask()
393 | state = .running
394 | return true
395 | } catch {
396 | _stream?.audioQueueInitializationFailed()
397 | return false
398 | }
399 | }
400 |
401 | private func endBackgroundTask() {
402 | #if os(iOS)
403 | guard _backgroundTask != UIBackgroundTaskInvalid else { return }
404 | UIApplication.shared.endBackgroundTask(_backgroundTask)
405 | _backgroundTask = UIBackgroundTaskInvalid
406 | #endif
407 | }
408 |
409 | private func startBackgroundTask() {
410 | #if os(iOS)
411 | if StreamConfiguration.shared.automaticAudioSessionHandlingEnabled {
412 | do {
413 | try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
414 | try AVAudioSession.sharedInstance().setActive(true)
415 | } catch {
416 | print("error:\(error)")
417 | }
418 | }
419 | endBackgroundTask()
420 | _backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: {[weak self] in
421 | self?.endBackgroundTask()
422 | })
423 | #endif
424 | }
425 |
426 | private func outputRenderCallback(ioActionFlags: UnsafeMutablePointer, inTimeStamp: UnsafePointer, inBusNumber: UInt32, inNumberFrames: UInt32, ioData: UnsafeMutablePointer?) -> OSStatus {
427 | let size = _pcmBufferFrameSizeInBytes * inNumberFrames
428 | ioData?.pointee.mBuffers.mNumberChannels = 2
429 | ioData?.pointee.mBuffers.mDataByteSize = size
430 | let raw = _buffers.advanced(by: _currentIndex * _pageSize)
431 | increaseBufferIndex()
432 | let readSize = UInt32(_provider!.read(bytes: raw, count: UInt(size)))
433 | var totalReadFrame: UInt32 = inNumberFrames
434 | if readSize == 0 {
435 | if _progress >= 1 {
436 | pause()
437 | } else {
438 | _stream?.audioQueueBuffersEmpty()
439 | ioActionFlags.pointee = AudioUnitRenderActionFlags.unitRenderAction_OutputIsSilence
440 | }
441 | memset(raw, 0, Int(size))
442 | return noErr
443 | } else if readSize != size {
444 | totalReadFrame = readSize / _pcmBufferFrameSizeInBytes
445 | let left = size - readSize
446 | memset(raw.advanced(by: Int(readSize)), 0, Int(left))
447 | }
448 | usedSize += Int(readSize)
449 | ioData?.pointee.mBuffers.mData = UnsafeMutableRawPointer(raw)
450 | _progress += Double(totalReadFrame) / AUPlayer.canonical.mSampleRate
451 | _stream?.audioQueueFinishedPlayingPacket()
452 | return noErr
453 | }
454 | }
455 |
456 | extension AUPlayer {
457 |
458 | struct Bus {
459 | static let output: UInt32 = 0
460 | static let input: UInt32 = 1
461 | }
462 |
463 | static var maxFramesPerSlice: UInt32 = 4096
464 | static let maxReadPerSlice: Int = Int(maxFramesPerSlice * canonical.mBytesPerPacket)
465 | static let minimumBufferCount: Int = 8
466 | static let minimumBufferSize: Int = maxReadPerSlice * minimumBufferCount
467 |
468 |
469 | static var outputUnit: AudioComponentDescription = {
470 | #if os(OSX)
471 | let subType = kAudioUnitSubType_DefaultOutput
472 | #else
473 | let subType = kAudioUnitSubType_RemoteIO
474 | #endif
475 | let component = AudioComponentDescription(componentType: kAudioUnitType_Output, componentSubType: subType, componentManufacturer: kAudioUnitManufacturer_Apple, componentFlags: 0, componentFlagsMask: 0)
476 | return component
477 | }()
478 |
479 | static var canonical: AudioStreamBasicDescription = {
480 | var bytesPerSample = UInt32(MemoryLayout.size)
481 | if #available(iOS 8.0, *) {
482 | bytesPerSample = UInt32(MemoryLayout.size)
483 | }
484 | let flags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked
485 | let component = AudioStreamBasicDescription(mSampleRate: 44100, mFormatID: kAudioFormatLinearPCM, mFormatFlags: flags, mBytesPerPacket: bytesPerSample * 2, mFramesPerPacket: 1, mBytesPerFrame: bytesPerSample * 2, mChannelsPerFrame: 2, mBitsPerChannel: 8 * bytesPerSample, mReserved: 0)
486 | return component
487 | }()
488 |
489 | static var canonicalSize: UInt32 = {
490 | return UInt32(MemoryLayout.size(ofValue: canonical))
491 | }()
492 |
493 | static var convertUnit: AudioComponentDescription = {
494 | let component = AudioComponentDescription(componentType: kAudioUnitType_FormatConverter, componentSubType: kAudioUnitSubType_AUConverter, componentManufacturer: kAudioUnitManufacturer_Apple, componentFlags: 0, componentFlagsMask: 0)
495 | return component
496 | }()
497 | static var mixer: AudioComponentDescription = {
498 | let component = AudioComponentDescription(componentType: kAudioUnitType_Mixer, componentSubType: kAudioUnitSubType_MultiChannelMixer, componentManufacturer: kAudioUnitManufacturer_Apple, componentFlags: 0, componentFlagsMask: 0)
499 | return component
500 | }()
501 |
502 | static func record() -> AudioStreamBasicDescription {
503 | var component = AudioStreamBasicDescription()
504 | component.mFormatID = kAudioFormatMPEG4AAC
505 | component.mFormatFlags = AudioFormatFlags(MPEG4ObjectID.AAC_LC.rawValue)
506 | component.mChannelsPerFrame = canonical.mChannelsPerFrame
507 | component.mSampleRate = canonical.mSampleRate
508 | return component
509 | }
510 |
511 | static var nbandUnit: AudioComponentDescription = {
512 | let component = AudioComponentDescription(componentType: kAudioUnitType_Effect, componentSubType: kAudioUnitSubType_NBandEQ, componentManufacturer: kAudioUnitManufacturer_Apple, componentFlags: 0, componentFlagsMask: 0)
513 | return component
514 | }()
515 | }
516 |
517 |
518 |
519 |
--------------------------------------------------------------------------------
/Sources/AudioQueue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AudioQueue.swift
3 | // FreePlayer
4 | //
5 | // Created by Lincoln Law on 2017/2/19.
6 | // Copyright © 2017年 Lincoln Law. All rights reserved.
7 | //
8 |
9 | import AudioToolbox
10 | // MARK: - AudioQueueDelegate
11 | protocol AudioQueueDelegate: class {
12 | func audioQueueStateChanged(state: State)
13 | func audioQueueBuffersEmpty()
14 | func audioQueueInitializationFailed()
15 | func audioQueueFinishedPlayingPacket()
16 | }
17 | // MARK: - AudioQueue
18 | final class AudioQueue {
19 |
20 | weak var delegate: AudioQueueDelegate?
21 | var streamDesc: AudioStreamBasicDescription?
22 | var initialOutputVolume: Float = 0
23 | var lastError: OSStatus = noErr
24 |
25 | var initialized: Bool { return _outAQ != nil }
26 |
27 | var volume: Float {
28 | get {
29 | guard let queue = _outAQ else { return 1 }
30 | var vol: Float = 0
31 | if AudioQueueGetParameter(queue, kAudioQueueParam_Volume, &vol) == noErr {
32 | return vol
33 | }
34 | return 1
35 | }
36 | set {
37 | guard let queue = _outAQ else { return }
38 | AudioQueueSetParameter(queue, kAudioQueueParam_Volume, newValue).check(operation: "set volume error")
39 | }
40 | }
41 |
42 | var currentState: State {
43 | var s = State.unknown
44 | _mutex.lock()
45 | s = _state
46 | _mutex.unlock()
47 | return s
48 | }
49 |
50 | private var _state: State = .unknown
51 | private var _outAQ: AudioQueueRef?// the audio queue
52 |
53 | private var _buffersWeakPointer: [UnsafeMutableRawPointer?] = []
54 | private var _audioQueueBuffer: UnsafeMutablePointer?// audio queue buffers
55 | private var _packetDescs: [AudioStreamPacketDescription] = []// packet descriptions for enqueuing audio
56 | // the index of the audioQueueBuffer that is being filled
57 | private var _fillBufferIndex = UInt32()
58 | // how many bytes have been filled
59 | private var _bytesFilled = UInt32()
60 | // how many packets have been filled
61 | private var _packetsFilled = UInt32()
62 | // how many buffers are used
63 | private var _buffersUsed = UInt32()
64 | // flag to indicate that the queue has been started
65 | private var _audioQueueStarted = false
66 | // flags to indicate that a buffer is still in use
67 | private var _bufferInUse: UnsafeMutablePointer?
68 | private var _levelMeteringEnabled = false
69 |
70 | private var _mutex: OSSpinLock = OS_SPINLOCK_INIT
71 | private var _bufferInUseMutex: pthread_mutex_t = .init()
72 | private var _bufferFreeCondition: pthread_cond_t = .init()
73 |
74 |
75 | deinit {
76 | stop(immediately: true)
77 | cleanup()
78 |
79 | let config = StreamConfiguration.shared
80 | let bufferCount = Int(config.bufferCount)
81 | _audioQueueBuffer?.deallocate(capacity: bufferCount)
82 | _bufferInUse?.deallocate(capacity: bufferCount)
83 |
84 | pthread_mutex_destroy(&_bufferInUseMutex)
85 | pthread_cond_destroy(&_bufferFreeCondition)
86 | aq_log("done")
87 | }
88 |
89 | init() {
90 | let config = StreamConfiguration.shared
91 | let bufferCount = Int(config.bufferCount)
92 | _packetDescs = Array(repeating: AudioStreamPacketDescription(), count: Int(config.maxPacketDescs))
93 | _audioQueueBuffer = UnsafeMutablePointer.allocate(capacity: bufferCount)
94 |
95 | _bufferInUse = UnsafeMutablePointer.allocate(capacity: bufferCount)
96 | for i in 0.. 2.0 { playRate = 2.0 }
200 | AudioQueueSetParameter(queue, kAudioQueueParam_PlayRate, playRate)
201 | }
202 |
203 | var currentTime: AudioTimeStamp {
204 | var queueTime: AudioTimeStamp = AudioTimeStamp()
205 | if let queue = _outAQ {
206 | var discontinuity: DarwinBoolean = false
207 | let err = AudioQueueGetCurrentTime(queue, nil, &queueTime, &discontinuity)
208 | if err != noErr {
209 | aq_log("AudioQueueGetCurrentTime failed")
210 | }
211 | }
212 | return queueTime
213 | }
214 |
215 | var levels: AudioQueueLevelMeterState {
216 | if !_levelMeteringEnabled, let queue = _outAQ {
217 | var enabledLevelMeter: Bool = true
218 | AudioQueueSetProperty(queue,
219 | kAudioQueueProperty_EnableLevelMetering,
220 | &enabledLevelMeter,
221 | UInt32(MemoryLayout.size))
222 | _levelMeteringEnabled = true
223 | }
224 |
225 | var levelMeter = AudioQueueLevelMeterState()
226 | if let queue = _outAQ {
227 | var levelMeterSize = UInt32(MemoryLayout.size)
228 | AudioQueueGetProperty(queue, kAudioQueueProperty_CurrentLevelMeterDB, &levelMeter, &levelMeterSize)
229 | }
230 | return levelMeter
231 | }
232 |
233 | func handleAudioPackets(inNumberBytes: UInt32, inNumberPackets: UInt32, inInputData: UnsafeRawPointer, inPacketDescriptions: UnsafeMutablePointer) {
234 | if !initialized {
235 | aq_log("warning: attempt to handle audio packets with uninitialized audio queue. return.")
236 | return
237 | }
238 | // this is called by audio file stream when it finds packets of audio
239 | // aq_log("got data. bytes: \(inNumberBytes) packets: \(inNumberPackets)")
240 | /* Place each packet into a buffer and then send each buffer into the audio
241 | queue */
242 | let total = Int(inNumberPackets)
243 | for i in 0.. bufferSize {
263 | aq_log("packetSize \(packetSize) > AQ_BUFSIZ \(bufferSize)")
264 | return
265 | }
266 |
267 | // if the space remaining in the buffer is not enough for this packet, then
268 | // enqueue the buffer and wait for another to become available.
269 | if (bufferSize - _bytesFilled < packetSize) {
270 | enqueueBuffer()
271 | if (!_audioQueueStarted) { return }
272 | } else {
273 | aq_log("skipped enqueueBuffer AQ_BUFSIZ - m_bytesFilled \(bufferSize) - \(_bytesFilled), packetSize \(packetSize)")
274 | }
275 |
276 | // copy data to the audio queue buffer
277 | if let buffer = _audioQueueBuffer?[Int(_fillBufferIndex)] {
278 | memcpy(buffer.pointee.mAudioData.advanced(by: Int(_bytesFilled)), inInputData.advanced(by: offset), Int(packetSize))
279 | desc.mStartOffset = Int64(_bytesFilled)
280 | }
281 |
282 |
283 | let index = Int(_packetsFilled)
284 | // fill out packet description to pass to enqueue() later on
285 | _packetDescs[index] = desc
286 | // Make sure the offset is relative to the start of the audio buffer
287 | _packetDescs[index].mStartOffset = Int64(_bytesFilled)
288 | // keep track of bytes filled and packets filled
289 | _bytesFilled += packetSize
290 | _packetsFilled += 1
291 |
292 | /* If filled our buffer with packets, then commit it to the system */
293 | if _packetsFilled >= UInt32(config.maxPacketDescs) { enqueueBuffer() }
294 | }
295 | }
296 |
297 | func reset(isSeeking: Bool = false) {
298 | guard var desc = streamDesc else {
299 | aq_log("streamDesc not validate!");
300 | return
301 | }
302 | cleanup()
303 | var err = noErr
304 | let this = UnsafeMutableRawPointer.from(object: self)
305 | err = AudioQueueNewOutput(&desc, AudioQueue.audioQueueOutputCallback, this, CFRunLoopGetCurrent(), nil, 0, &_outAQ)
306 | guard let queue = _outAQ else {
307 | if err != noErr {
308 | aq_log("error in AudioQueueNewOutput")
309 | lastError = err
310 | }
311 | delegate?.audioQueueInitializationFailed()
312 | return
313 | }
314 |
315 | guard let audioQueueBuffer = _audioQueueBuffer else {
316 | aq_log("_audioQueueBuffer not validate")
317 | return
318 | }
319 |
320 | // allocate audio queue buffers
321 | let configuration = StreamConfiguration.shared
322 | let bufferCount = Int(configuration.bufferCount)
323 | for i in 0...allocate(capacity: 1)
326 | defer{ free(raw) }
327 | err = AudioQueueAllocateBuffer(queue, UInt32(configuration.bufferSize), raw)
328 | buffer.pointee = raw.pointee
329 | if err != noErr {
330 | /* If allocating the buffers failed, everything else will fail, too.
331 | * Dispose the queue so that we can later on detect that this
332 | * queue in fact has not been initialized.
333 | */
334 | aq_log("error in AudioQueueAllocateBuffer")
335 | AudioQueueDispose(queue, true)
336 | _outAQ = nil
337 | lastError = err
338 | delegate?.audioQueueInitializationFailed()
339 | return
340 | }
341 | }
342 |
343 | // listen for kAudioQueueProperty_IsRunning
344 | err = AudioQueueAddPropertyListener(queue, kAudioQueueProperty_IsRunning, AudioQueue.audioQueueIsRunningCallback, this)
345 | if err != lastError {
346 | aq_log("error in AudioQueueAddPropertyListener")
347 | lastError = err
348 | return
349 | }
350 | if configuration.enableTimeAndPitchConversion {
351 | var enableTimePitchConversion = UInt32(1)
352 | err = AudioQueueSetProperty (queue, kAudioQueueProperty_EnableTimePitch, &enableTimePitchConversion, UInt32(MemoryLayout.size))
353 | if err != noErr {
354 | aq_log("Failed to enable time and pitch conversion. Play rate setting will fail")
355 | }
356 | }
357 | if isSeeking {
358 | volume = 0
359 | } else if initialOutputVolume != 1.0 {
360 | volume = initialOutputVolume
361 | }
362 | }
363 | }
364 |
365 | // MARK: - Private
366 | extension AudioQueue {
367 |
368 | private func cleanup() {
369 |
370 | _mutex.lock()
371 | guard let queue = _outAQ else {
372 | _mutex.unlock()
373 | aq_log("warning: attempt to cleanup an uninitialized audio queue. return.")
374 | return
375 | }
376 | _mutex.unlock()
377 | let config = StreamConfiguration.shared
378 | let cstate = currentState
379 | if cstate != .idle {
380 | aq_log("attemping to cleanup the audio queue when it is still playing, force stopping")
381 | let this = Unmanaged.passRetained(self).toOpaque()
382 | AudioQueueRemovePropertyListener(queue, kAudioQueueProperty_IsRunning, AudioQueue.audioQueueIsRunningCallback, this)
383 | AudioQueueStop(queue, true)
384 | state = .idle
385 | }
386 | _mutex.lock()
387 | if (AudioQueueDispose(queue, true) != 0) { aq_log("AudioQueueDispose failed!"); }
388 | _outAQ = nil
389 | _mutex.unlock()
390 | _fillBufferIndex = 0
391 | _bytesFilled = 0
392 | _packetsFilled = 0
393 | _buffersUsed = 0
394 |
395 | let bufferCount = Int(config.bufferCount)
396 | for i in 0.. 0)
421 |
422 | let err = AudioQueueEnqueueBuffer(queue, fillBuf, _packetsFilled, &_packetDescs)
423 |
424 | if err == noErr {
425 | lastError = noErr
426 | start()
427 | } else {
428 | /* If we get an error here, it very likely means that the audio queue is no longer
429 | running */
430 | aq_log("error in AudioQueueEnqueueBuffer")
431 | lastError = err
432 | return
433 | }
434 |
435 | pthread_mutex_lock(&_bufferInUseMutex)
436 | // go to next buffer
437 | _fillBufferIndex += 1
438 | if _fillBufferIndex >= UInt32(config.bufferCount) {
439 | _fillBufferIndex = 0
440 | }
441 | // reset bytes filled
442 | _bytesFilled = 0
443 | // reset packets filled
444 | _packetsFilled = 0
445 |
446 | // wait until next buffer is not in use
447 | while (_bufferInUse?[Int(_fillBufferIndex)] == true) {
448 | // aq_log("waiting for buffer \(_fillBufferIndex)")
449 | pthread_cond_wait(&_bufferFreeCondition, &_bufferInUseMutex)
450 | }
451 | pthread_mutex_unlock(&_bufferInUseMutex)
452 | }
453 | }
454 |
455 | // MARK: - Call back
456 | extension AudioQueue {
457 | // MARK: CallBacks
458 | private static var audioQueueIsRunningCallback: AudioQueuePropertyListenerProc {
459 | return { inClientData, inAQ, inID in
460 | guard let audioQueue = inClientData?.to(object: AudioQueue.self) else { return }
461 | aq_log("enter")
462 | var running = UInt32()
463 | var output = UInt32(MemoryLayout.size)
464 | let err = AudioQueueGetProperty(inAQ, kAudioQueueProperty_IsRunning, &running, &output)
465 | if err != noErr {
466 | aq_log("error in kAudioQueueProperty_IsRunning")
467 | return
468 | }
469 | if running != 0 {
470 | aq_log("audio queue running!")
471 | audioQueue.state = .running
472 | } else {
473 | audioQueue.state = .idle
474 | }
475 | }
476 | }
477 |
478 | // this is called by the audio queue when it has finished decoding our data.
479 | // The buffer is now free to be reused.
480 | private static var audioQueueOutputCallback: AudioQueueOutputCallback {
481 | return { (inClientData, inAQ, inBuffer) in
482 | guard let audioQueue = inClientData?.to(object: AudioQueue.self), let audioQueueBuffer = audioQueue._audioQueueBuffer else { return }
483 |
484 | let config = StreamConfiguration.shared
485 | let bufferCount = Int(config.bufferCount)
486 | var bufIndex = -1
487 | for i in 0.. URL? {
60 | guard let p = path else { return nil }
61 | let escapePath = p.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
62 | return URL(fileURLWithPath: escapePath ?? p)
63 | }
64 |
65 | private func readMetaData() {
66 | guard let url = _metaDataUrl else { return }
67 | guard let stream = CFReadStreamCreateWithFile(kCFAllocatorDefault, url as CFURL) else { return }
68 | guard CFReadStreamOpen(stream) else { return }
69 | var buf: [UInt8] = Array(repeating: 0, count: 1024)
70 | let bytesRead = CFReadStreamRead(stream, &buf, 1024)
71 | if bytesRead > 0 {
72 | guard let type = String(bytes: buf, encoding: .utf8) else { return }
73 | cs_log("Setting the content type [\(type)] of the file stream based on the meta data")
74 | _fileStream.contentType = contentType
75 | }
76 | CFReadStreamClose(stream)
77 | }
78 |
79 | func setStoreIdentifier(id: String) -> Bool {
80 | _storeIdentifier = id
81 | _fileOutput = nil
82 | let config = StreamConfiguration.shared
83 | guard let storeFolder = config.storeDirectory else {
84 | cs_log("no config.storeDirectory)")
85 | return false
86 | }
87 |
88 | let filePath = (storeFolder as NSString).appendingPathComponent(id)
89 | let metaDataPath = (storeFolder as NSString).appendingPathComponent(id + ".metadata")
90 |
91 | let buffer = filePath.withCString{ $0 }
92 | var b: stat = stat()
93 | let hasFile = stat(buffer, &b) == 0
94 | if !hasFile {
95 | cs_log("no file:\(filePath) at:\(config.storeDirectory ?? "")")
96 | return false
97 | }
98 |
99 | guard let url = createFileURL(with: filePath) else {
100 | cs_log("createFileURL:\(filePath) fail")
101 | return false
102 | }
103 | _fileUrl = url
104 | _metaDataUrl = createFileURL(with: metaDataPath)
105 | _fileStream.set(url: url)
106 | cs_log("success")
107 | return true
108 | }
109 |
110 | func setCacheIdentifier(id: String) {
111 | _cacheIdentifier = id
112 | _fileOutput = nil
113 | let config = StreamConfiguration.shared
114 | let cacheFolder = config.cacheDirectory
115 | let filePath = (cacheFolder as NSString).appendingPathComponent(id)
116 | let metaDataPath = (cacheFolder as NSString).appendingPathComponent(id + ".metadata")
117 |
118 | let buffer = metaDataPath.withCString{ $0 }
119 | var b: stat = stat()
120 | let hasFile = stat(buffer, &b) == 0
121 | cachedComplete = hasFile
122 |
123 | guard let url = createFileURL(with: filePath) else { return }
124 | _fileUrl = url
125 | _metaDataUrl = createFileURL(with: metaDataPath)
126 | _fileStream.set(url: url)
127 | }
128 |
129 | static func canHandleUrl(url: URL?) -> Bool {
130 | return true
131 | }
132 | }
133 | // MARK: StreamInputProtocol
134 | extension CachingStream: StreamInputProtocol {
135 |
136 | @discardableResult public func open(_ position: Position) -> Bool {
137 | var status = false
138 | if let meta = _metaDataUrl, CFURLResourceIsReachable(meta as CFURL, nil), let file = _fileUrl, CFURLResourceIsReachable(file as CFURL, nil) {
139 | _cacheable = false
140 | _writable = false
141 | _useCache = true
142 | _cacheMetaDataWritten = false
143 |
144 | readMetaData()
145 | cs_log("Playing file from cache")
146 | cs_log("file:\(file)")
147 | status = _fileStream.open(position)
148 | } else {
149 | _cacheable = false
150 | _writable = false
151 | _useCache = false
152 | _cacheMetaDataWritten = false
153 | cs_log("File not cached")
154 | status = _target.open(position)
155 | }
156 | return status
157 | }
158 |
159 | @discardableResult public func open() -> Bool {
160 |
161 | var status = false
162 | if let meta = _metaDataUrl, CFURLResourceIsReachable(meta as CFURL, nil), let file = _fileUrl, CFURLResourceIsReachable(file as CFURL, nil) {
163 | _cacheable = false
164 | _writable = false
165 | _useCache = true
166 | _cacheMetaDataWritten = false
167 | readMetaData()
168 | cs_log("Playing file from cache")
169 | status = _fileStream.open()
170 | } else {
171 | _cacheable = true
172 | _writable = false
173 | _useCache = false
174 | _cacheMetaDataWritten = false
175 | status = _target.open()
176 | }
177 | return status
178 | }
179 |
180 | public func close() {
181 | _fileStream.close()
182 | _target.close()
183 | }
184 |
185 | public func setScheduledInRunLoop(run: Bool) {
186 | _useCache ? _fileStream.setScheduledInRunLoop(run: run) : _target.setScheduledInRunLoop(run: run)
187 | }
188 |
189 | public func set(url: URL) { _target.set(url: url) }
190 | }
191 |
192 | // MARK: - StreamInputDelegate
193 | extension CachingStream: StreamInputDelegate {
194 | public func streamIsReadyRead() {
195 | if _cacheable {
196 | // If the stream is cacheable (not seeked from some position)
197 | // Check if the stream has a length. If there is no length,
198 | // it is a continuous stream and thus cannot be cached.
199 | _cacheable = _target.contentLength > 0
200 | }
201 | delegate?.streamIsReadyRead()
202 | }
203 |
204 | public func streamHasBytesAvailable(data: UnsafePointer, numBytes: UInt32) {
205 | if _cacheable {
206 | guard numBytes > 0 else { return }
207 | if _fileOutput == nil, let url = _fileUrl {
208 | cs_log("Caching started for stream")
209 | _fileOutput = StreamOutputManager(fileURL: url)
210 | _writable = true
211 | }
212 | if _writable, let out = _fileOutput {
213 | _writable = out.write(data: data, length: Int(numBytes)) && _writable
214 | }
215 | }
216 | delegate?.streamHasBytesAvailable(data: data, numBytes: numBytes)
217 | }
218 |
219 | public func streamEndEncountered() {
220 | _fileOutput = nil
221 | if _cacheable, _writable {
222 | cs_log("Successfully cached the stream\n:\(_fileUrl!)")
223 | // We only write the meta data if the stream was successfully streamed.
224 | // In that way we can use the meta data as an indicator that there is a file to stream.
225 | if !_cacheMetaDataWritten, let url = _metaDataUrl {
226 | cs_log("Writing the meta data")
227 | let contentType = _target.contentType
228 | do {
229 | try contentType.data(using: .utf8)?.write(to: url)
230 | } catch {
231 | cs_log("Writing the meta data error:\(error)")
232 | }
233 | _cacheable = false
234 | _writable = false
235 | _useCache = true
236 | _cacheMetaDataWritten = true
237 | }
238 | }
239 | delegate?.streamEndEncountered()
240 | }
241 |
242 | public func streamErrorOccurred(errorDesc: String) {
243 | delegate?.streamErrorOccurred(errorDesc: errorDesc)
244 | }
245 |
246 | public func streamMetaDataAvailable(metaData: [String: Metadata]) {
247 | delegate?.streamMetaDataAvailable(metaData: metaData)
248 | }
249 |
250 | public func streamMetaDataByteSizeAvailable(sizeInBytes: UInt32) {
251 | delegate?.streamMetaDataByteSizeAvailable(sizeInBytes: sizeInBytes)
252 | }
253 |
254 | public func streamHasDataCanPlay() -> Bool {
255 | return delegate?.streamHasDataCanPlay() ?? false
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/Sources/DisplayLink.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DisplayLink.swift
3 | // FreePlayer
4 | //
5 | // Created by lincolnlaw on 2017/7/26.
6 | // Copyright © 2017年 LawLincoln. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import QuartzCore
11 |
12 | #if !os(OSX)
13 | typealias DisplayLink = CADisplayLink
14 | extension CADisplayLink {
15 | func start() {
16 | isPaused = false
17 | }
18 | func stop() {
19 | isPaused = true
20 | }
21 | }
22 | #else
23 | typealias DisplayLink = CVDisplayLink
24 | extension CVDisplayLink {
25 | func invalidate() {
26 | stop()
27 | }
28 | func start() {
29 | CVDisplayLinkStart(self)
30 | }
31 | func stop() {
32 | CVDisplayLinkStop(self)
33 | }
34 | }
35 | #endif
36 |
37 | public final class FreeDisplayLink {
38 |
39 | private var _callbackHanlder: () -> Void = { }
40 | private var displayLink: DisplayLink?
41 |
42 | init(update: () -> Void) {
43 | createDisplayLink()
44 | }
45 |
46 | private func update() {
47 | _callbackHanlder()
48 | }
49 |
50 | func invalidate() {
51 | displayLink?.invalidate()
52 | }
53 |
54 | func stop() {
55 | displayLink?.stop()
56 | }
57 |
58 | func start() {
59 | displayLink?.start()
60 | }
61 |
62 | var isPaused: Bool = false {
63 | didSet {
64 | isPaused ? displayLink?.stop() : displayLink?.start()
65 | }
66 | }
67 |
68 | }
69 | extension FreeDisplayLink {
70 | #if !os(OSX)
71 |
72 | func createDisplayLink() {
73 | displayLink = CADisplayLink(target: self, selector: #selector(newFrame(_:)))
74 | displayLink?.add(to: .main, forMode: .commonModes)
75 | }
76 |
77 | @objc private func newFrame(_ displayLink: CADisplayLink) {
78 | update()
79 | }
80 | #else
81 | func createDisplayLink() {
82 | func callback(link: CVDisplayLink,
83 | inNow: UnsafePointer, //wtf is this?
84 | inOutputTime: UnsafePointer,
85 | flagsIn: CVOptionFlags,
86 | flagsOut: UnsafeMutablePointer,
87 | displayLinkContext: UnsafeMutableRawPointer?) -> CVReturn {
88 | unsafeBitCast(displayLinkContext, to: FreeDisplayLink.self).update()
89 | return kCVReturnSuccess
90 | }
91 |
92 | CVDisplayLinkCreateWithActiveCGDisplays(&displayLink)
93 | guard let displayLink = displayLink else {
94 | fatalError("Unable to create a CVDisplayLink?")
95 | }
96 | CVDisplayLinkSetOutputCallback(displayLink, callback, unsafeBitCast(self, to: UnsafeMutableRawPointer.self))
97 | CVDisplayLinkStart(displayLink)
98 | }
99 | #endif
100 | }
101 |
--------------------------------------------------------------------------------
/Sources/Extension.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Extension.swift
3 | // FreePlayer
4 | //
5 | // Created by Lincoln Law on 2017/2/20.
6 | // Copyright © 2017年 Lincoln Law. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | extension OSSpinLock {
12 | mutating func lock() { OSSpinLockLock(&self) }
13 | mutating func unlock() { OSSpinLockUnlock(&self) }
14 | }
15 | extension Array {
16 | subscript(fp_safe index: Int) -> Element? {
17 | return indices ~= index ? self[index] : nil
18 | }
19 | }
20 | extension UnsafeMutableRawPointer {
21 | func to(object: T.Type) -> T {
22 | return Unmanaged.fromOpaque(self).takeUnretainedValue()
23 | }
24 | static func from(object: T) -> UnsafeMutableRawPointer {
25 | return Unmanaged.passUnretained(object).toOpaque()
26 | }
27 | }
28 |
29 | extension OSStatus {
30 | enum OSStatusError: Error {
31 | case faile(String)
32 | }
33 | private func humanErrorMessage(from raw: String) -> String {
34 | var result = ""
35 | switch raw {
36 | case "wht?": result = "Audio File Unspecified"
37 | case "typ?": result = "Audio File Unsupported File Type"
38 | case "fmt?": result = "Audio File Unsupported Data Format"
39 | case "pty?": result = "Audio File Unsupported Property"
40 | case "!siz": result = "Audio File Bad Property Size"
41 | case "prm?": result = "Audio File Permissions Error"
42 | case "optm": result = "Audio File Not Optimized"
43 | case "chk?": result = "Audio File Invalid Chunk"
44 | case "off?": result = "Audio File Does Not Allow 64Bit Data Size"
45 | case "pck?": result = "Audio File Invalid Packet Offset"
46 | case "dta?": result = "Audio File Invalid File"
47 | case "op??", "0x6F703F3F": result = "Audio File Operation Not Supported"
48 | case "!pkd": result = "Audio Converter Err Requires Packet Descriptions Error"
49 | case "-38": result = "Audio File Not Open"
50 | case "-39": result = "Audio File End Of File Error"
51 | case "-40": result = "Audio File Position Error"
52 | case "-43": result = "Audio File File Not Found"
53 | default: result = ""
54 | }
55 | result = "\(result)(\(raw))"
56 | return result
57 | }
58 |
59 | @discardableResult func check(operation: String = "", file: String = #file, method: String = #function, line: Int = #line) -> String? {
60 | guard self != noErr else { return nil }
61 |
62 | var result: String = ""
63 | var char = Int(bigEndian)
64 |
65 | for _ in 0..<4 {
66 | guard isprint(Int32(char&255)) == 1 else {
67 | result = "\(self)"
68 | break
69 | }
70 | //UnicodeScalar(char&255) will get optional
71 | let raw = String(describing: UnicodeScalar(UInt8(char&255)))
72 | result += raw
73 | char = char/256
74 | }
75 | let humanMsg = humanErrorMessage(from: result)
76 | let msg = "\n{\n file: \(file):\(line),\n function: \(method),\n operation: \(operation),\n message: \(humanMsg)\n}"
77 | print(msg)
78 | return msg
79 | }
80 |
81 | func throwCheck(file: String = #file, method: String = #function, line: Int = #line) throws {
82 | guard let msg = check(file: file, method: method, line: line) else { return }
83 | throw OSStatusError.faile(msg)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Sources/FileStream.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileStream.swift
3 | // FreePlayer
4 | //
5 | // Created by Lincoln Law on 2017/2/21.
6 | // Copyright © 2017年 Lincoln Law. All rights reserved.
7 | //
8 |
9 | import AudioToolbox
10 |
11 | /// FileStream
12 | final class FileStream {
13 | weak var delegate: StreamInputDelegate?
14 | private var _url: URL?
15 | private var _readStream: CFReadStream?
16 | private var _scheduledInRunLoop = false
17 | private var _readPending = false
18 | private var _contentType: String?
19 | private var _position = Position()
20 | private var _fileReadBuffer: UnsafeMutablePointer?
21 | private var _id3Parser: ID3Parser?
22 | deinit {
23 | close()
24 | _id3Parser = nil
25 | let config = StreamConfiguration.shared
26 | _fileReadBuffer?.deallocate(capacity: Int(config.httpConnectionBufferSize))
27 | }
28 |
29 | init() {
30 | _id3Parser = ID3Parser()
31 | _id3Parser?.delegate = self
32 | }
33 | }
34 |
35 | // MARK: - ID3ParserDelegate
36 | extension FileStream: ID3ParserDelegate {
37 | func id3metaDataAvailable(metaData: [String : Metadata]) {
38 | delegate?.streamMetaDataAvailable(metaData: metaData)
39 | }
40 |
41 | func id3tagSizeAvailable(tag size: UInt32) {
42 | delegate?.streamMetaDataByteSizeAvailable(sizeInBytes: size)
43 | }
44 | }
45 | // MARK: - StreamInputProtocol
46 | extension FileStream: StreamInputProtocol {
47 |
48 | var position: Position { return _position }
49 |
50 | var contentType: String {
51 | get {
52 | if let type = _contentType { return type }
53 | guard let lastComponent = _url?.lastPathComponent else { return "" }
54 | let contentTypes = [".mp3" : "mpeg",".m4a" : "x-m4a", ".aac" : "aac"]
55 | for (key, value) in contentTypes {
56 | if lastComponent.hasSuffix(key) { return "audio/\(value)" }
57 | }
58 | return "audio/mpeg"
59 | }
60 | set { _contentType = newValue }
61 | }
62 |
63 | var contentLength: UInt64 {
64 | guard let u = _url else { return 0 }
65 | var pError: Unmanaged? = nil
66 | var pSize: CFNumber? = nil
67 | CFURLCopyResourcePropertyForKey(u as CFURL, kCFURLFileSizeKey, &pSize, &pError)
68 | if let size = pSize {
69 | return UInt64(truncating: size)
70 | }
71 | return 0
72 | }
73 |
74 | var errorDescription: String? { return nil }
75 |
76 | func open(_ position: Position) -> Bool {
77 | var success = false
78 | let this = UnsafeMutableRawPointer.voidPointer(from: self)
79 | var ctx = CFStreamClientContext(version: 0, info: this, retain: nil, release: nil, copyDescription: nil)
80 | func out() -> Bool {
81 | if success { delegate?.streamIsReadyRead() }
82 | return success
83 | }
84 | /* Already opened a read stream, return */
85 | if _readStream != nil { return out() }
86 | guard let url = _url else { return out() }
87 |
88 | /* Reset state */
89 | _position = position
90 | _readPending = false
91 |
92 | /* Failed to create a stream */
93 | _readStream = CFReadStreamCreateWithFile(kCFAllocatorDefault, url as CFURL)
94 | if _readStream == nil { return out() }
95 |
96 | if _position.start > 0 {
97 | let position = CFNumberCreate(kCFAllocatorDefault, .longLongType, &_position.start)
98 | CFReadStreamSetProperty(_readStream, CFStreamPropertyKey.fileCurrentOffset, position)
99 | }
100 |
101 | let flags = CFStreamEventType.hasBytesAvailable.rawValue | CFStreamEventType.endEncountered.rawValue | CFStreamEventType.errorOccurred.rawValue
102 | let result = CFReadStreamSetClient(_readStream, flags, FileStream.readCallBack, &ctx)
103 | if result == false { return out() }
104 | setScheduledInRunLoop(run: true)
105 |
106 | if !CFReadStreamOpen(_readStream) {
107 | /* Open failed: clean */
108 | CFReadStreamSetClient(_readStream, 0, nil, nil)
109 | setScheduledInRunLoop(run: false)
110 | _readStream = nil
111 | return out()
112 | }
113 | success = true
114 | _id3Parser?.detechV1(with: url, total: 0)
115 | return out()
116 | }
117 |
118 | func open() -> Bool {
119 | _id3Parser?.reset()
120 | return open(Position())
121 | }
122 |
123 | func close() {
124 | guard let readStream = _readStream else { return }
125 | CFReadStreamSetClient(readStream, 0, nil, nil)
126 | setScheduledInRunLoop(run: false)
127 | CFReadStreamClose(readStream)
128 | _readStream = nil
129 | }
130 |
131 | func setScheduledInRunLoop(run: Bool) {
132 | guard let readStream = _readStream else { return }
133 | if run == false {
134 | CFReadStreamUnscheduleFromRunLoop(readStream, CFRunLoopGetCurrent(), CFRunLoopMode.commonModes)
135 | } else {
136 | if _readPending {
137 | _readPending = false
138 | let this = UnsafeMutableRawPointer.voidPointer(from: self)
139 | FileStream.readCallBack(readStream, CFStreamEventType.hasBytesAvailable, this)
140 | }
141 | CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(), CFRunLoopMode.commonModes)
142 | }
143 | _scheduledInRunLoop = run
144 | }
145 |
146 | func set(url: URL) { _url = url }
147 | }
148 |
149 | // MARK: readCallBack
150 | extension FileStream {
151 | static var readCallBack: CFReadStreamClientCallBack {
152 | return { stream, type, userData in
153 | guard let data = userData else { return }
154 | let fs = data.to(object: FileStream.self)
155 | let config = StreamConfiguration.shared
156 |
157 | func errorOccurred() {
158 | guard let error = CFReadStreamCopyError(stream), let desc = CFErrorCopyDescription(error) else { return }
159 | fs.delegate?.streamErrorOccurred(errorDesc: desc as String)
160 | }
161 |
162 | func hasBytesAvailable() {
163 | let size = Int(config.httpConnectionBufferSize)
164 | if fs._fileReadBuffer == nil {
165 | fs._fileReadBuffer = UnsafeMutablePointer.allocate(capacity: size)
166 | }
167 |
168 | while CFReadStreamHasBytesAvailable(stream) {
169 | if fs._scheduledInRunLoop == false {
170 | /*
171 | * This is critical - though the stream has data available,
172 | * do not try to feed the audio queue with data, if it has
173 | * indicated that it doesn't want more data due to buffers
174 | * full.
175 | */
176 | fs._readPending = true
177 | break
178 | }
179 | let bytesRead = CFReadStreamRead(stream, fs._fileReadBuffer, size)
180 | if CFReadStreamGetStatus(stream) == CFStreamStatus.error || bytesRead < 0 {
181 | errorOccurred()
182 | }
183 | if bytesRead > 0, let buffer = fs._fileReadBuffer {
184 | let len = UInt32(bytesRead)
185 | fs.delegate?.streamHasBytesAvailable(data: buffer, numBytes: len)
186 | if fs._id3Parser?.wantData() == true {
187 | fs._id3Parser?.feedData(data: buffer, numBytes: len)
188 | }
189 | }
190 | }
191 | }
192 | func endEncountered() { fs.delegate?.streamEndEncountered() }
193 | switch type {
194 | case CFStreamEventType.hasBytesAvailable: hasBytesAvailable()
195 | case CFStreamEventType.endEncountered: endEncountered()
196 | case CFStreamEventType.errorOccurred: errorOccurred()
197 | default: break
198 | }
199 |
200 | }
201 | }
202 | }
203 |
204 | // MARK: canHandleURL
205 | extension FileStream {
206 | static func canHandle(url: URL?) -> Bool {
207 | guard let u = url else { return false }
208 | return u.scheme == "file"
209 | }
210 | }
211 |
--------------------------------------------------------------------------------
/Sources/FreePlayer+MPPlayingCenter.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FreePlayer+MPPlayingCenter.swift
3 | // FreePlayer
4 | //
5 | // Created by Lincoln Law on 2017/3/1.
6 | // Copyright © 2017年 Lincoln Law. All rights reserved.
7 | //
8 |
9 | import MediaPlayer
10 |
11 | public final class NowPlayingInfo: NSObject {
12 | public static var shared = NowPlayingInfo()
13 | var session = URLSession.shared
14 | #if os(iOS)
15 | public var name = "" { didSet { FPLogger.write(msg: "🎹:\(name)") } }
16 | public var artist = ""
17 | public var album = ""
18 | public var artwork: UIImage? = nil { didSet { didSetImage() } }
19 | public var duration = 0
20 | public var playbackRate = Double()
21 | public var playbackTime = Double()
22 | public var didSetImage: () -> () = {}
23 | private var _lock: OSSpinLock = OS_SPINLOCK_INIT
24 | private var _coverTask: URLSessionDataTask?
25 | private var _backgroundTask = UIBackgroundTaskInvalid
26 | private override init() { }
27 |
28 | var info: [String : Any] {
29 | var map = [String : Any]()
30 | _lock.lock()
31 | map[MPMediaItemPropertyTitle] = name
32 | map[MPMediaItemPropertyArtist] = artist
33 | map[MPMediaItemPropertyAlbumTitle] = album
34 | map[MPNowPlayingInfoPropertyElapsedPlaybackTime] = playbackTime
35 | map[MPNowPlayingInfoPropertyPlaybackRate] = playbackRate
36 | map[MPMediaItemPropertyPlaybackDuration] = duration
37 | if let image = artwork {
38 | map[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image: image)
39 | }
40 | _lock.unlock()
41 | return map
42 | }
43 |
44 | public func play(elapsedPlayback: Double) {
45 | playbackTime = elapsedPlayback
46 | playbackRate = 1
47 | update()
48 | }
49 |
50 | public func pause(elapsedPlayback: Double) {
51 | playbackTime = elapsedPlayback
52 | playbackRate = 0
53 | update()
54 | }
55 |
56 | public func image(with url: String?) {
57 | guard let u = url, let r = URL(string: u) else { return }
58 | _coverTask?.cancel()
59 | endBackgroundTask()
60 | DispatchQueue.global(qos: .utility).async {
61 | let request = URLRequest(url: r, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 20)
62 | if let d = URLCache.shared.cachedResponse(for: request)?.data, let image = UIImage(data: d) {
63 | // DispatchQueue.main.async {
64 | self._lock.lock()
65 | self.artwork = image
66 | self._lock.unlock()
67 | self.update()
68 | // }
69 | return
70 | }
71 | self.startBackgroundTask()
72 | let task = self.session.dataTask(with: request, completionHandler: { [weak self](data, resp, _) in
73 | self?.endBackgroundTask()
74 | if let r = resp, let d = data {
75 | let cre = CachedURLResponse(response: r, data: d)
76 | URLCache.shared.storeCachedResponse(cre, for: request)
77 | }
78 | // DispatchQueue.main.async {
79 | guard let sself = self, let d = data, let image = UIImage(data: d) else { return }
80 | sself._lock.lock()
81 | sself.artwork = image
82 | sself._lock.unlock()
83 | sself.update()
84 | // }
85 | })
86 | task.resume()
87 | self._coverTask = task
88 | }
89 | }
90 |
91 | public func update() {
92 | DispatchQueue.main.async {
93 | MPNowPlayingInfoCenter.default().nowPlayingInfo = self.info
94 | }
95 | }
96 |
97 | public func remove() {
98 | _lock.lock()
99 | name = ""
100 | artist = ""
101 | album = ""
102 | artwork = UIImage()
103 | duration = 0
104 | playbackRate = Double()
105 | playbackTime = Double()
106 | _lock.unlock()
107 | DispatchQueue.main.async {
108 | MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
109 | }
110 | }
111 |
112 | private func startBackgroundTask() {
113 | endBackgroundTask()
114 | _backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: {[weak self] in
115 | self?.endBackgroundTask()
116 | })
117 | }
118 |
119 | private func endBackgroundTask() {
120 | guard _backgroundTask != UIBackgroundTaskInvalid else { return }
121 | UIApplication.shared.endBackgroundTask(_backgroundTask)
122 | _backgroundTask = UIBackgroundTaskInvalid
123 | }
124 | #endif
125 | func updateProxy() {
126 | let config = StreamConfiguration.shared
127 | if config.usingCustomProxy {
128 | guard config.customProxyHttpHost.isEmpty == false, config.customProxyHttpPort != 0 else { return }
129 | let configure = URLSessionConfiguration.default
130 | let enableKey = kCFNetworkProxiesHTTPEnable as String
131 | let hostKey = kCFNetworkProxiesHTTPProxy as String
132 | let portKey = kCFNetworkProxiesHTTPPort as String
133 | configure.connectionProxyDictionary = [
134 | enableKey : 1,
135 | hostKey : config.customProxyHttpHost,
136 | portKey : config.customProxyHttpPort
137 | ]
138 | session = URLSession(configuration: configure, delegate: self, delegateQueue: .main)
139 | } else {
140 | NowPlayingInfo.shared.session = URLSession.shared
141 | }
142 | }
143 |
144 | }
145 |
146 | // MARK: - URLSessionDelegate, URLSessionTaskDelegate
147 | extension NowPlayingInfo: URLSessionDelegate, URLSessionTaskDelegate {
148 | public func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
149 | let config = StreamConfiguration.shared
150 | let isDigestMehod = challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPDigest
151 | if challenge.previousFailureCount == 0, isDigestMehod {
152 | let credential = URLCredential(user: config.customProxyUsername, password: config.customProxyPassword, persistence: URLCredential.Persistence.forSession)
153 | completionHandler(URLSession.AuthChallengeDisposition.useCredential, credential)
154 | } else {
155 | completionHandler(URLSession.AuthChallengeDisposition.useCredential, nil)
156 | }
157 | }
158 | }
159 |
160 |
--------------------------------------------------------------------------------
/Sources/FreePlayer.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FreePlayer.swift
3 | // FreePlayer
4 | //
5 | // Created by Lincoln Law on 2017/2/28.
6 | // Copyright © 2017年 Lincoln Law. All rights reserved.
7 | //
8 |
9 | import AVFoundation
10 | #if os(iOS)
11 | import UIKit
12 | import AudioToolbox
13 | #endif
14 |
15 | public final class FreePlayer {
16 | public var maxRetryCount = 3
17 | public var onStateChange: ((AudioStreamState) -> Void)?
18 | public var onComplete: (() -> Void)?
19 | public var onFailure: ((AudioStreamError, String?) -> Void)?
20 | #if !os(OSX)
21 | public var networkPermisionHandler: FPNetworkUsingPermisionHandler? {
22 | didSet { _audioStream.networkPermisionHandler = networkPermisionHandler }
23 | }
24 | #endif
25 |
26 | private lazy var _audioStream: AudioStream = {
27 | let a = AudioStream()
28 | a.delegate = self
29 | return a
30 | }()
31 |
32 | private var _url: URL?
33 | private var _reachability: Reachability?
34 | #if !os(OSX)
35 | private var _backgroundTask = UIBackgroundTaskInvalid
36 | #endif
37 | private var _lastSeekByteOffset: Position?
38 | private var _propertyLock: OSSpinLock = OS_SPINLOCK_INIT
39 | private var _internetConnectionAvailable = true
40 |
41 | private var _wasInterrupted = false
42 | private var _wasDisconnected = false
43 | private var _wasPaused = false
44 |
45 | private var _retryCount = 0
46 | private var _stopHandlerNetworkChange = false
47 |
48 | deinit {
49 | assert(Thread.isMainThread)
50 | NotificationCenter.default.removeObserver(self)
51 | stop()
52 | FreePlayer.removeIncompleteCache()
53 | }
54 |
55 | public init() {
56 | startReachability()
57 | addInteruptOb()
58 | }
59 |
60 | public convenience init(url target: URL) {
61 | self.init()
62 | _url = target
63 | reset()
64 | }
65 |
66 |
67 |
68 | private func addInteruptOb() {
69 | #if !os(OSX)
70 | /// RouteChange
71 | NotificationCenter.default.addObserver(forName: .AVAudioSessionRouteChange, object: nil, queue: OperationQueue.main) { [weak self](note) -> Void in
72 | let interuptionDict = note.userInfo
73 | // "Headphone/Line was pulled. Stopping player...."
74 | if let routeChangeReason = interuptionDict?[AVAudioSessionRouteChangeReasonKey] as? UInt, routeChangeReason == AVAudioSessionRouteChangeReason.oldDeviceUnavailable.rawValue {
75 | self?.pause()
76 | }
77 | }
78 |
79 | var playingStateBeforeInterrupte = false
80 | NotificationCenter.default.addObserver(forName: .AVAudioSessionInterruption, object: nil, queue: nil) { [weak self](note) -> Void in
81 | guard let sself = self else { return }
82 | let info = note.userInfo
83 | guard let type = info?[AVAudioSessionInterruptionTypeKey] as? UInt else { return }
84 | if type == AVAudioSessionInterruptionType.began.rawValue {
85 | // 中断开始
86 | playingStateBeforeInterrupte = sself.isPlaying
87 | if playingStateBeforeInterrupte == true { sself.pause() }
88 | } else {
89 | // 中断结束
90 | guard let options = info?[AVAudioSessionInterruptionOptionKey] as? UInt, options == AVAudioSessionInterruptionOptions.shouldResume.rawValue, playingStateBeforeInterrupte == true else { return }
91 | sself.resume()
92 | }
93 | }
94 | #endif
95 | }
96 |
97 |
98 | private func reset() {
99 | _audioStream.reset()
100 | #if os(iOS)
101 | _audioStream.networkPermisionHandler = networkPermisionHandler
102 | #endif
103 | _audioStream.set(url: url, completion: {[unowned self] (success) in
104 | DispatchQueue.main.async {
105 | self._internetConnectionAvailable = true
106 | self._retryCount = 0
107 | self.play()
108 | }
109 | })
110 | _retryCount = 0
111 | _internetConnectionAvailable = true
112 | #if os(iOS)
113 | _backgroundTask = UIBackgroundTaskInvalid
114 | if StreamConfiguration.shared.automaticAudioSessionHandlingEnabled {
115 | do {
116 | try AVAudioSession.sharedInstance().setCategory(AVAudioSessionCategoryPlayback)
117 | } catch {
118 | fp_log("error:\(error)")
119 | }
120 | }
121 | #endif
122 | }
123 | }
124 |
125 | // MARK: Public
126 | extension FreePlayer {
127 |
128 | public func togglePlayPause() {
129 | assert(Thread.isMainThread)
130 | _wasPaused ? resume() : pause()
131 | }
132 |
133 | public func pause() {
134 | assert(Thread.isMainThread)
135 | _wasPaused = true
136 | _audioStream.pause()
137 | }
138 |
139 | public func resume() {
140 | assert(Thread.isMainThread)
141 | _wasPaused = false
142 | _audioStream.resume()
143 | #if os(iOS)
144 | NowPlayingInfo.shared.play(elapsedPlayback: Double(playbackPosition.timePlayed))
145 | #endif
146 | }
147 |
148 | public func play(from target: URL?){
149 | guard let u = target else { return }
150 | _url = u
151 | reset()
152 | play()
153 | }
154 |
155 | public func play() {
156 | assert(Thread.isMainThread)
157 |
158 | self._wasPaused = false
159 | if _audioStream.isPreloading {
160 | _audioStream.startCachedDataPlayback()
161 | return
162 | }
163 | #if os(iOS)
164 | self.endBackgroundTask()
165 | self._backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: {[weak self] in
166 | self?.endBackgroundTask()
167 | })
168 | #endif
169 | _audioStream.open()
170 | self.startReachability()
171 |
172 | /*
173 | guard let audio = _audioStream else { return }
174 | _wasPaused = false
175 | if audio.isPreloading {
176 | audio.startCachedDataPlayback()
177 | return
178 | }
179 | #if os(iOS)
180 | endBackgroundTask()
181 | _backgroundTask = UIApplication.shared.beginBackgroundTask(expirationHandler: {[weak self] in
182 | self?.endBackgroundTask()
183 | })
184 | #endif
185 | audio.open()
186 | startReachability()
187 | */
188 | }
189 |
190 | public func stop() {
191 | assert(Thread.isMainThread)
192 | _audioStream.forceStop = true
193 | _audioStream.reset()
194 | endBackgroundTask()
195 | _stopHandlerNetworkChange = true
196 | }
197 |
198 | public var volume: Float {
199 | get { return _audioStream.volume }
200 | set { _audioStream.volume = newValue }
201 | }
202 |
203 | public func rewind(in seconds: UInt) {
204 | DispatchQueue.main.async {
205 | let audio = self._audioStream
206 | if self.durationInSeconds <= 0 { return } // Rewinding only possible for continuous streams
207 | let oriVolume = self.volume
208 | audio.volume = 0
209 | audio.rewind(in: seconds)
210 | DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {[weak self] in
211 | self?._audioStream.volume = oriVolume
212 | })
213 | }
214 | }
215 |
216 | public func preload() {
217 | DispatchQueue.main.async {
218 | self._audioStream.setPreloading(loading: true)
219 | self._audioStream.open()
220 | }
221 | }
222 |
223 | public func seek(to time: Float) {
224 | DispatchQueue.main.async {
225 | let duration = self.durationInSeconds
226 | if duration <= 0 { return }
227 | var offset = time / duration
228 | if offset > 1 { offset = 1 }
229 | if offset < 0 { offset = 0 }
230 | self._audioStream.resume()
231 | self._audioStream.seek(to: offset)
232 | #if os(iOS)
233 | NowPlayingInfo.shared.play(elapsedPlayback: Double(self.playbackPosition.timePlayed))
234 | #endif
235 | }
236 | }
237 |
238 | public func setPlayRate(to value: Float) {
239 | DispatchQueue.main.async {
240 | self._audioStream.set(playRate: value)
241 | }
242 | }
243 |
244 | public var isPlaying: Bool {
245 | assert(Thread.isMainThread)
246 | let state = _audioStream.state
247 | return state == .playing || state == .endOfFile
248 | }
249 |
250 | // MARK: audio properties
251 | public var fileHint: AudioFileTypeID {
252 | assert(Thread.isMainThread)
253 | return _audioStream.fileHint
254 | }
255 |
256 | public var contentLength: UInt {
257 | assert(Thread.isMainThread)
258 | return _audioStream.contentLength
259 | }
260 |
261 | public var defaultContentLength: UInt {
262 | assert(Thread.isMainThread)
263 | return _audioStream.defaultContentLength
264 | }
265 |
266 | public var bufferRatio: Float {
267 | assert(Thread.isMainThread)
268 | let audio = _audioStream
269 | let length = Float(audio.contentLength)
270 | let read = Float(audio.bytesReceived)
271 | var final = length > 0 ? read / length : 0
272 | if final > 1 { final = 1 }
273 | if final < 0 { final = 0 }
274 | return final
275 | }
276 |
277 | public var prebufferedByteCount: Int {
278 | assert(Thread.isMainThread)
279 | return _audioStream.cachedDataSize
280 | }
281 |
282 | public var durationInSeconds: Float {
283 | assert(Thread.isMainThread)
284 | return _audioStream.duration
285 | }
286 |
287 | public var currentSeekByteOffset: Position {
288 | assert(Thread.isMainThread)
289 | var offset = Position()
290 | let audio = _audioStream
291 | if durationInSeconds <= 0 { return offset }// continuous
292 | offset.position = audio.playBackPosition.offset
293 | let pos = audio.streamPosition(for: offset.position)
294 | offset.start = pos.start
295 | offset.end = pos.end
296 | return offset
297 | }
298 |
299 | public var bitRate: Float {
300 | assert(Thread.isMainThread)
301 | return _audioStream.bitrate
302 | }
303 |
304 | public var formatDescription: String {
305 | assert(Thread.isMainThread)
306 | return _audioStream.sourceFormatDescription
307 | }
308 |
309 | public var playbackPosition: PlaybackPosition {
310 | assert(Thread.isMainThread)
311 | return _audioStream.playBackPosition
312 | }
313 |
314 | public var cached: Bool {
315 | assert(Thread.isMainThread)
316 | guard let url = _url else { return false }
317 | var config = StreamConfiguration.shared
318 | let fs = FileManager.default
319 | let id = config.cacheNaming.name(for: url)
320 | let cachedFile = (config.cacheDirectory as NSString).appendingPathComponent(id)
321 | var result = fs.fileExists(atPath: cachedFile)
322 | let additionalFolder = config.cachePolicy.additionalFolder
323 | if result == false, let storeFolder = additionalFolder {
324 | let storedFile = (storeFolder as NSString).appendingPathComponent(id)
325 | result = fs.fileExists(atPath: storedFile)
326 | }
327 | return result
328 | }
329 |
330 | public var url: URL? {
331 | get {
332 | assert(Thread.isMainThread)
333 | return _url
334 | }
335 | set {
336 | _propertyLock.lock()
337 | if _url == newValue {
338 | _propertyLock.unlock()
339 | return
340 | }
341 | _url = newValue
342 | _audioStream.set(url: newValue, completion: {[unowned self] (success) in
343 | DispatchQueue.main.async {
344 | self._internetConnectionAvailable = true
345 | self._retryCount = 0
346 | self.play()
347 | }
348 | })
349 | _propertyLock.unlock()
350 | }
351 | }
352 |
353 | // MARK: methods
354 | private func endBackgroundTask() {
355 | #if os(iOS)
356 | guard _backgroundTask != UIBackgroundTaskInvalid else { return }
357 | UIApplication.shared.endBackgroundTask(_backgroundTask)
358 | _backgroundTask = UIBackgroundTaskInvalid
359 | #endif
360 | }
361 |
362 | func notify(state: AudioStreamState) {
363 | switch state {
364 | case .stopped:
365 | #if os(iOS)
366 | if StreamConfiguration.shared.automaticAudioSessionHandlingEnabled {
367 | try? AVAudioSession.sharedInstance().setActive(false)
368 | }
369 | DispatchQueue.main.async {
370 | NowPlayingInfo.shared.pause(elapsedPlayback: Double(self.playbackPosition.timePlayed))
371 | }
372 | #endif
373 | case .buffering: _internetConnectionAvailable = true
374 | case .playing:
375 | #if os(iOS)
376 | if StreamConfiguration.shared.automaticAudioSessionHandlingEnabled {
377 | try? AVAudioSession.sharedInstance().setActive(true)
378 | }
379 | DispatchQueue.main.async {
380 | let duration = Int(ceil(self.durationInSeconds))
381 | if NowPlayingInfo.shared.duration != duration {
382 | NowPlayingInfo.shared.duration = duration
383 | }
384 | if NowPlayingInfo.shared.playbackRate != 1 {
385 | NowPlayingInfo.shared.play(elapsedPlayback: Double(self.playbackPosition.timePlayed))
386 | }
387 | }
388 | #endif
389 | if _retryCount > 0 {
390 | _retryCount = 0
391 | onStateChange?(.retryingSucceeded)
392 | }
393 | endBackgroundTask()
394 | case .paused:
395 | #if os(iOS)
396 | DispatchQueue.main.async {
397 | NowPlayingInfo.shared.pause(elapsedPlayback: Double(self.playbackPosition.timePlayed))
398 | }
399 | #endif
400 | case .failed:
401 | endBackgroundTask()
402 | #if os(iOS)
403 | NowPlayingInfo.shared.remove()
404 | #endif
405 | case .playbackCompleted: onComplete?()
406 | default: break
407 | }
408 | onStateChange?(state)
409 | }
410 |
411 | func attemptRestart() {
412 | let audio = _audioStream
413 |
414 | if audio.isPreloading {
415 | debug_log("☄️: Stream is preloading. Not attempting a restart")
416 | return
417 | }
418 |
419 | if _wasPaused {
420 | debug_log("☄️: Stream was paused. Not attempting a restart")
421 | return
422 | }
423 |
424 | if _internetConnectionAvailable == false {
425 | debug_log("☄️: Internet connection not available. Not attempting a restart")
426 | return
427 | }
428 |
429 | if _retryCount >= maxRetryCount {
430 | debug_log("☄️: Retry count \(_retryCount). Giving up.")
431 | DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {[weak self] in
432 | self?.notify(state: .retryingFailed)
433 | })
434 | return
435 | }
436 |
437 | debug_log("☄️: Attempting restart.")
438 | DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {[weak self] in
439 | self?.notify(state: .retryingStarted)
440 | self?.play()
441 | })
442 | _retryCount += 1
443 | }
444 |
445 |
446 | private func handleStateChanged(info: Reachability) {
447 | if _stopHandlerNetworkChange { return }
448 | DispatchQueue.main.async {
449 | let status = info.currentReachabilityStatus
450 | self._internetConnectionAvailable = status != .notReachable
451 | if self.isPlaying && self._internetConnectionAvailable == false {
452 | self._wasDisconnected = true
453 | debug_log("☄️: Internet connection disconnected while playing a stream.")
454 | }
455 | if self._wasDisconnected && self._internetConnectionAvailable {
456 | self._wasDisconnected = false
457 | if self._audioStream.streamHasDataCanPlay() == false {
458 | self.attemptRestart()
459 | }
460 | debug_log("☄️: Internet connection available again.")
461 | }
462 | }
463 | }
464 |
465 | private func startReachability() {
466 | _stopHandlerNetworkChange = false
467 | guard _reachability == nil else { return }
468 | _reachability = Reachability(hostname: "www.baidu.com")
469 | _reachability?.whenReachable = {[weak self] info in
470 | self?.handleStateChanged(info: info)
471 | }
472 | _reachability?.whenUnreachable = { [weak self] info in
473 | self?.handleStateChanged(info: info)
474 | }
475 | try? _reachability?.startNotifier()
476 | }
477 |
478 | func isWifiAvailable() -> Bool {
479 | return _reachability?.isReachableViaWiFi ?? false
480 | }
481 | }
482 |
483 | // MARK: - AudioStreamDelegate
484 | extension FreePlayer: AudioStreamDelegate {
485 | func audioStreamStateChanged(state: AudioStreamState) {
486 | fp_log("state:\(state)")
487 | func run() { notify(state: state) }
488 | #if !os(OSX)
489 | if #available(iOS 10.0, *) {
490 | RunLoop.current.perform {
491 | self.notify(state: state)
492 | }
493 | } else {
494 | run()
495 | }
496 | #else
497 | if #available(OSX 10.12, *) {
498 | RunLoop.current.perform {
499 | self.notify(state: state)
500 | }
501 | } else {
502 | run()
503 | }
504 | #endif
505 | }
506 |
507 | func audioStreamErrorOccurred(errorCode: AudioStreamError , errorDescription: String) {
508 | onFailure?(errorCode, errorDescription)
509 | let needRestart: [AudioStreamError] = [.network, .unsupportedFormat, .open, .terminated]
510 | if _audioStream.isPreloading == false && needRestart.contains(errorCode) {
511 | attemptRestart()
512 | fp_log("audioStreamErrorOccurred attemptRestart")
513 | }
514 | }
515 |
516 | func audioStreamMetaDataAvailable(metaData: [MetaDataKey : Metadata]) {
517 | #if os(iOS)
518 | guard StreamConfiguration.shared.autoFillID3InfoToNowPlayingCenter else { return }
519 | DispatchQueue.global(qos: .utility).async {
520 | if let value = metaData[.title], case Metadata.text(let title) = value {
521 | NowPlayingInfo.shared.name = title
522 | }
523 | if let value = metaData[.album], case Metadata.text(let album) = value {
524 | NowPlayingInfo.shared.album = album
525 | }
526 | if let value = metaData[.artist], case Metadata.text(let artist) = value {
527 | NowPlayingInfo.shared.artist = artist
528 | }
529 | if let value = metaData[.cover], case Metadata.data(let cover) = value {
530 | NowPlayingInfo.shared.artwork = UIImage(data: cover)
531 | }
532 | NowPlayingInfo.shared.update()
533 | }
534 |
535 | #endif
536 | }
537 |
538 | func samplesAvailable(samples: UnsafeMutablePointer, frames: UInt32, description: AudioStreamPacketDescription) {
539 |
540 | }
541 |
542 | func bitrateAvailable() {
543 | let config = StreamConfiguration.shared
544 |
545 | guard config.usePrebufferSizeCalculationInSeconds == false else { return }
546 |
547 | let bitrate = _audioStream.bitrate
548 | if bitrate <= 0 { return } // No bitrate provided, use the defaults
549 |
550 | let bufferSizeForSecond = bitrate / 8.0
551 |
552 | var bufferSize = bufferSizeForSecond * Float(config.requiredPrebufferSizeInSeconds)
553 |
554 | if bufferSize < 50000 { bufferSize = 50000 } // Check that we still got somewhat sane buffer size
555 |
556 | if self.durationInSeconds <= 0 {
557 | // continuous
558 | if bufferSize > Float(config.requiredInitialPrebufferedByteCountForContinuousStream) {
559 | bufferSize = Float(config.requiredInitialPrebufferedByteCountForContinuousStream)
560 | }
561 | } else {
562 | if bufferSize > Float(config.requiredInitialPrebufferedByteCountForNonContinuousStream) {
563 | bufferSize = Float(config.requiredInitialPrebufferedByteCountForNonContinuousStream)
564 | }
565 | }
566 | // Update the configuration
567 | StreamConfiguration.shared.requiredInitialPrebufferedByteCountForContinuousStream = Int(bufferSize)
568 | StreamConfiguration.shared.requiredInitialPrebufferedByteCountForNonContinuousStream = Int(bufferSize)
569 | }
570 | }
571 |
572 | // MARK: Static Function
573 | extension FreePlayer {
574 |
575 | public static func totalCachedObjectsSize() -> UInt64 {
576 | var total = UInt64()
577 | let fs = FileManager.default
578 | let dir = StreamConfiguration.shared.cacheDirectory
579 | do {
580 | let folder = dir as NSString
581 | let files = try fs.contentsOfDirectory(atPath: dir)
582 | for item in files {
583 | let path = folder.appendingPathComponent(item)
584 | let attributes = try fs.attributesOfItem(atPath: path)
585 | if let size = attributes[FileAttributeKey.size] as? UInt64 {
586 | total += size
587 | }
588 | }
589 | } catch { debug_log(error) }
590 | return total
591 | }
592 |
593 | public static func expungeCacheFolder() {
594 | DispatchQueue.global(qos: .utility).async {
595 | let fs = FileManager.default
596 | let dir = StreamConfiguration.shared.cacheDirectory
597 | do {
598 | try fs.removeItem(atPath: dir)
599 | try fs.createDirectory(atPath: dir, withIntermediateDirectories: true, attributes: nil)
600 | } catch {
601 | debug_log(error)
602 | }
603 | }
604 | }
605 |
606 | public static func removeCache(by url: URL?) {
607 | guard let raw = url else { return }
608 | DispatchQueue.global(qos: .utility).async {
609 | let fs = FileManager.default
610 | let id = StreamConfiguration.shared.cacheNaming.name(for: raw)
611 | let dir = StreamConfiguration.shared.cacheDirectory
612 | do {
613 | let folder = dir as NSString
614 | let files = try fs.contentsOfDirectory(atPath: dir)
615 | for item in files {
616 | guard item.hasPrefix(id) else { continue }
617 | let path = folder.appendingPathComponent(item)
618 | try fs.removeItem(atPath: path)
619 | }
620 | } catch {
621 | debug_log(error)
622 | }
623 | }
624 | }
625 |
626 | public static func removeIncompleteCache() {
627 | DispatchQueue.global(qos: .utility).async {
628 | let fs = FileManager.default
629 | let dir = StreamConfiguration.shared.cacheDirectory
630 | do {
631 | let folder = dir as NSString
632 | let files = try fs.contentsOfDirectory(atPath: dir)
633 | for item in files {
634 | if item.hasSuffix(".tmp") {
635 | let path = folder.appendingPathComponent(item)
636 | try fs.removeItem(atPath: path)
637 | }
638 | }
639 | } catch {
640 | debug_log(error)
641 | }
642 | }
643 | }
644 | }
645 |
646 |
--------------------------------------------------------------------------------
/Sources/Global.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Global.swift
3 | // FreePlayer
4 | //
5 | // Created by Lincoln Law on 2017/2/28.
6 | // Copyright © 2017年 Lincoln Law. All rights reserved.
7 | //
8 |
9 | import AudioToolbox
10 |
11 | // MARK: - Structure
12 | public enum AudioStreamState: Int { case stopped, buffering, playing, paused, seeking, failed, endOfFile, playbackCompleted, retryingStarted, retryingSucceeded, retryingFailed }
13 |
14 | public enum AudioStreamError: Int {
15 | case none, open, streamParse, network, unsupportedFormat, streamBouncing, terminated, networkPermission, badURL
16 | }
17 |
18 | public struct PlaybackPosition {
19 | public var offset = Float()
20 | public var timePlayed = Float()
21 | public init() { }
22 | }
23 |
24 | public struct Position {
25 | public var start: UInt = 0
26 | public var end: UInt = 0
27 | public var position = Float()
28 | public init() { }
29 | }
30 |
31 | public typealias FPNetworkUsingPermisionHandler = (@escaping (Bool) -> Void) -> Void
32 |
33 |
--------------------------------------------------------------------------------
/Sources/ID3Parser.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ID3Parser.swift
3 | // FreePlayer
4 | //
5 | // Created by Lincoln Law on 2017/3/5.
6 | // Copyright © 2017年 Lincoln Law. All rights reserved.
7 | //
8 | import Foundation
9 |
10 | protocol ID3ParserDelegate: class {
11 | func id3metaDataAvailable(metaData: [MetaDataKey : Metadata])
12 | func id3tagSizeAvailable(tag size: UInt32)
13 | func id3tagParsingDone()
14 | }
15 |
16 | final class ID3Parser {
17 |
18 | weak var delegate: ID3ParserDelegate?
19 | private lazy var _state: State = .initial
20 | private lazy var _tagData: Data = Data()
21 | private lazy var _bytesReceived = UInt32()
22 | private lazy var _majorVersion = UInt8()
23 | private lazy var _hasFooter = false
24 | private lazy var _usesUnsynchronisation = false
25 | private lazy var _usesExtendedHeader = false
26 | private lazy var _title = ""
27 | private lazy var _album = ""
28 | private lazy var _performer = ""
29 | private lazy var _coverArt: Data? = nil
30 | private lazy var _lastData: Data? = nil
31 | private lazy var _lock: OSSpinLock = OS_SPINLOCK_INIT
32 | private lazy var _parsing = false
33 | private lazy var _syncQueue = DispatchQueue(label: "id3.sync")
34 | // private lazy var _queue = RunloopQueue(named: "StreamProvider.id3.parser")
35 | private var _hasV1Tag = false { didSet { totalTagSize() } }
36 | private var _tagSize = UInt32() { didSet { totalTagSize() } }
37 | private lazy var _v1TagDeal = false
38 | private lazy var _v2TagDeal = false
39 | enum State {
40 | case initial
41 | case parseFrames
42 | case tagParsed
43 | case notID3V2
44 | }
45 |
46 | private struct ID3V1Length {
47 | static var header: Int { return 3 }
48 | static var title: Int { return 30 }
49 | static var artist: Int { return 30 }
50 | static var album: Int { return 30 }
51 | static var year: Int { return 4 }
52 | static var comment: Int { return 30 }
53 | static var genre: Int { return 4 }
54 | }
55 |
56 | deinit { id3_log("ID3Parser deinit") }
57 |
58 | init() {}
59 | }
60 |
61 | extension ID3Parser {
62 | func setState(state: State) {
63 | _state = state
64 | if state == .tagParsed {
65 | delegate?.id3tagParsingDone()
66 | }
67 | }
68 |
69 | func parseContent(framesize: UInt32, pos: UInt32, encoding: CFStringEncoding, byteOrderMark: Bool) -> String {
70 | func un(raw: UnsafeMutablePointer) -> UnsafeMutablePointer { return raw }
71 | let pointer = _tagData.withUnsafeMutableBytes({ (item: UnsafeMutablePointer) in
72 | item
73 | }).advanced(by: Int(pos))
74 | guard framesize > 1 else { return "" }
75 | let size = framesize - 1
76 | return CFStringCreateWithBytes(kCFAllocatorDefault, pointer, Int(size), encoding, byteOrderMark) as String
77 | }
78 | }
79 |
80 | extension ID3Parser {
81 |
82 | func reset() {
83 | _state = .initial
84 | _bytesReceived = 0
85 | _majorVersion = 0
86 | _v1TagDeal = false
87 | _v2TagDeal = false
88 | _tagSize = 0
89 | _hasFooter = false
90 | _usesUnsynchronisation = false
91 | _usesExtendedHeader = false
92 | _tagData.removeAll()
93 | }
94 |
95 | func wantData() -> Bool {
96 | let done = [State.tagParsed].contains(_state)
97 | return done == false
98 | }
99 |
100 | func totalTagSize() {
101 | guard _v1TagDeal, _v2TagDeal else { return }
102 | var final = _tagSize
103 | if _hasV1Tag { final += 128 }
104 | delegate?.id3tagSizeAvailable(tag: final)
105 | }
106 |
107 | func detechV1(with url: URL?, total: UInt) {
108 | DispatchQueue.global(qos: .utility).async {
109 | guard let u = url else {
110 | self._v1TagDeal = true
111 | self._hasV1Tag = false
112 | return
113 | }
114 | let scheme = u.scheme?.lowercased()
115 | let isLocal = scheme == "file"
116 | let isRemote = isLocal == false
117 | if isRemote {
118 | if total < 128 {
119 | self._v1TagDeal = true
120 | self._hasV1Tag = false
121 | return
122 | }
123 | var request = URLRequest(url: u)
124 | request.setValue("bytes=-128", forHTTPHeaderField: "Range")
125 | NowPlayingInfo.shared.session.dataTask(with: request) { [weak self] data, _, _ in
126 | if let d = data, d.count == 4 {
127 | let range = Range(uncheckedBounds: (d.startIndex, d.startIndex.advanced(by: 3)))
128 | let sub = d.subdata(in: range)
129 | let tag = String(data: sub, encoding: .ascii)
130 | if tag == "TAG" {
131 | DispatchQueue.main.async {
132 | self?._v1TagDeal = true
133 | self?._hasV1Tag = true
134 | }
135 | return
136 | }
137 | }
138 | DispatchQueue.main.async {
139 | self?._v1TagDeal = true
140 | self?._hasV1Tag = false
141 | }
142 | }.resume()
143 | } else if isLocal {
144 | let raw = u.absoluteString.replacingOccurrences(of: "file://", with: "")
145 | var buff = stat()
146 | if stat(raw.withCString({ $0 }), &buff) != 0 {
147 | DispatchQueue.main.async {
148 | self._v1TagDeal = true
149 | self._hasV1Tag = false
150 | }
151 | return
152 | }
153 | let size = buff.st_size
154 | if let file = fopen(raw.withCString({ $0 }), "r".withCString({ $0 })) {
155 | defer { fclose(file) }
156 | fseek(file, -128, SEEK_END)
157 | let length = 3
158 | var bytes: [UInt8] = Array(repeating: 0, count: length)
159 | fread(&bytes, 1, length, file)
160 | if String(bytes: bytes, encoding: .utf8) == "TAG" {
161 | DispatchQueue.main.async {
162 | self._v1TagDeal = true
163 | self._hasV1Tag = true
164 | }
165 | return
166 | }
167 | }
168 | DispatchQueue.main.async {
169 | self._v1TagDeal = true
170 | self._hasV1Tag = false
171 | }
172 | }
173 | }
174 | }
175 |
176 | func feedData(data: UnsafeMutablePointer, numBytes: UInt32) {
177 | _syncQueue.sync {
178 | if self.wantData() == false { return }
179 | if self._state == .tagParsed { return }
180 | if self._parsing { return }
181 | self._bytesReceived += numBytes
182 | let bytesSize = Int(numBytes)
183 | let raw = malloc(bytesSize)!.assumingMemoryBound(to: UInt8.self)
184 | defer { free(raw) }
185 | memcpy(raw, data, bytesSize)
186 | let dat = Data(bytes: raw, count: bytesSize)
187 | self._lastData = dat
188 | if self._state != .notID3V2 {
189 | self._tagData.append(dat)
190 | }
191 | var canParseFrames = false
192 | if self._state == .initial {
193 | canParseFrames = self.initial()
194 | } else if self._state == .parseFrames {
195 | canParseFrames = true
196 | } else if self._state == .notID3V2 {
197 | var realData = dat
198 | var len = dat.count
199 | if len < 128, let d = self._lastData {
200 | id3_log("append last data")
201 | realData = d + dat
202 | len = realData.count
203 | }
204 | if len < 128 {
205 | id3_log("try parser id3v1 but len:\(len) to short")
206 | return
207 | }
208 | let start = len - 128
209 | let v1 = realData[start ..< len]
210 | let tag = v1.map({ $0 })[0 ..< 3]
211 | let raw = String(bytes: tag, encoding: .ascii)
212 | if raw == "TAG" {
213 | self.dealV1(with: v1.map({ $0 }))
214 | }
215 | }
216 | if canParseFrames, self._parsing == false {
217 | self.parseFrames()
218 | }
219 | }
220 | }
221 |
222 | private func dealV1(with data: [UInt8]) {
223 | id3_log("tag size: \(_tagSize)")
224 | if StreamConfiguration.shared.autoFillID3InfoToNowPlayingCenter == false { return }
225 |
226 | let total = [ID3V1Length.header, ID3V1Length.title, ID3V1Length.artist, ID3V1Length.album, ID3V1Length.year, ID3V1Length.comment]
227 | var offset = 0
228 | var end = 0
229 | for (i, len) in total.enumerated() {
230 | if i == 0 {
231 | offset += len
232 | continue
233 | }
234 | end = len + offset
235 | let t = offset ..< end
236 | let range = data[t].flatMap({ $0 })
237 | let value = String(bytes: range, encoding: .ascii)?.replacingOccurrences(of: "\0", with: "") ?? ""
238 | switch i {
239 | case 1: _title = value
240 | case 2: _performer = value
241 | default: break
242 | }
243 | offset += len
244 | }
245 | DispatchQueue.main.async {
246 | self.setState(state: .tagParsed)
247 | self._tagData.removeAll()
248 | self._parsing = false
249 | }
250 | // Push out the metadata
251 | if let d = delegate {
252 | var metadataMap = [MetaDataKey: Metadata]()
253 | if _performer.isEmpty == false {
254 | metadataMap[.artist] = .text(_performer)
255 | }
256 | if _title.isEmpty == false {
257 | metadataMap[.title] = .text(_title)
258 | }
259 | if metadataMap.count > 0 {
260 | DispatchQueue.main.async { d.id3metaDataAvailable(metaData: metadataMap) }
261 | }
262 | }
263 | }
264 |
265 | private func initial() -> Bool {
266 | // Do we have enough bytes to determine if this is an ID3 tag or not?
267 | /*
268 | char Header[3]; /* 必须为"ID3"否则认为标签不存在 */
269 | char Ver; /* 版本号;ID3V2.3就记录03,ID3V2.4就记录04 */
270 | char Revision; /* 副版本号;此版本记录为00 */
271 | char Flag; /* 存放标志的字节,这个版本只定义了三位,稍后详细解说 */
272 | char Size[4]; /* 标签大小,包括标签帧和标签头。(不包括扩展标签头的10个字节) */
273 | */
274 | if _bytesReceived <= 9 { return false }
275 | let sub = _tagData[0 ... 2]
276 | let content = String(bytes: sub, encoding: .ascii)
277 | if content != "ID3" {
278 | id3_log("Not an ID3v2 tag, bailing out")
279 | setState(state: .notID3V2)
280 | _v2TagDeal = true
281 | return false
282 | }
283 | _majorVersion = _tagData[3]
284 | // Currently support only id3v2.2 and 2.3
285 | if _majorVersion != 2 && _majorVersion != 3 && _majorVersion != 4 {
286 | id3_log("ID3v2.\(_majorVersion) not supported by the parser")
287 | _v2TagDeal = true
288 | setState(state: .notID3V2)
289 | return false
290 | }
291 | // Ignore the revision
292 | // Parse the flags
293 | if _tagData[5] & 0x80 != 0 {
294 | _usesUnsynchronisation = true
295 | } else if _tagData[5] & 0x40 != 0, _majorVersion >= 3 {
296 | _usesExtendedHeader = true
297 | } else if _tagData[5] & 0x10 != 0, _majorVersion >= 3 {
298 | _hasFooter = true
299 | }
300 | let six = (UInt32(_tagData[6]) & 0x7F) << 21
301 | let seven = (UInt32(_tagData[7]) & 0x7F) << 14
302 | let eight = (UInt32(_tagData[8]) & 0x7F) << 7
303 | let nine = UInt32(_tagData[9]) & 0x7F
304 | var tagsize = six | seven | eight | nine
305 |
306 | if tagsize > 0 {
307 | if _hasFooter { tagsize += 10 }
308 | tagsize += 10
309 | _v2TagDeal = true
310 | _tagSize = tagsize
311 | id3_log("tag size: \(_tagSize)")
312 | if StreamConfiguration.shared.autoFillID3InfoToNowPlayingCenter == false {
313 | setState(state: .tagParsed)
314 | return false
315 | } else {
316 | setState(state: .parseFrames)
317 | return true
318 | }
319 | }
320 | _v2TagDeal = true
321 | setState(state: .notID3V2)
322 | return false
323 | }
324 |
325 | private func parseFrames() {
326 | // Do we have enough data to parse the frames?
327 | if _tagData.count < Int(_tagSize) {
328 | id3_log("Not enough data received for parsing, have \(_tagData.count) bytes, need \(_tagSize) bytes")
329 | DispatchQueue.main.async { self._parsing = false }
330 | return
331 | }
332 | _parsing = true
333 | var pos = 10
334 | // Do we have an extended header? If we do, skip it
335 | if _usesExtendedHeader {
336 | let i = UInt32(_tagData[pos])
337 | let ii = UInt32(_tagData[pos + 1])
338 | let iii = UInt32(_tagData[pos + 2])
339 | let iv = UInt32(_tagData[pos + 3])
340 | // let extendedHeaderSize = Int((i << 21) | (ii << 14) | (iii << 7) | iv)
341 | let array = [i, ii, iii, iv]
342 | let extendedHeaderSize = array.toInt(offsetSize: 7)
343 |
344 | if pos + extendedHeaderSize >= Int(_tagSize) {
345 | DispatchQueue.main.async {
346 | self.setState(state: .notID3V2)
347 | self._parsing = false
348 | }
349 | return
350 | }
351 | id3_log("Skipping extended header, size \(extendedHeaderSize)")
352 | pos += extendedHeaderSize
353 | }
354 | parsing(from: pos)
355 | doneParsing()
356 | }
357 |
358 | private func parsing(from position: Int) {
359 | var pos = position
360 | let total = Int(_tagSize)
361 | while pos < total {
362 | var frameName: [UInt8] = Array(repeatElement(0, count: 4))
363 | frameName[0] = _tagData[pos]
364 | frameName[1] = _tagData[pos + 1]
365 | frameName[2] = _tagData[pos + 2]
366 |
367 | if _majorVersion >= 3 { frameName[3] = _tagData[pos + 3] }
368 | else { frameName[3] = 0 }
369 |
370 | var framesize = 0
371 | var i: UInt32
372 | var ii: UInt32
373 | var iii: UInt32
374 | var iv: UInt32
375 | if _majorVersion >= 3 {
376 | pos += 4
377 | i = UInt32(_tagData[pos])
378 | ii = UInt32(_tagData[pos + 1])
379 | iii = UInt32(_tagData[pos + 2])
380 | iv = UInt32(_tagData[pos + 3])
381 | // let a = (i << 21)
382 | // let b = (ii << 14)
383 | // let c = (iii << 7)
384 | let array = [i, ii, iii, iv]
385 | framesize = array.toInt(offsetSize: 7)
386 | // framesize = Int( a + b + c + iv)
387 | } else {
388 | i = UInt32(_tagData[pos])
389 | ii = UInt32(_tagData[pos + 1])
390 | iii = UInt32(_tagData[pos + 2])
391 | iv = 0
392 | let array = [i, ii, iii, iv]
393 | framesize = array.toInt(offsetSize: 8)
394 | // framesize = Int((i << 16) + (ii << 8) + iii)
395 | }
396 |
397 | if framesize == 0 {
398 | DispatchQueue.main.async {
399 | self.setState(state: .notID3V2)
400 | self._parsing = false
401 | }
402 | break
403 | // Break from the loop and then out of the case context
404 | }
405 |
406 | if _majorVersion >= 3 { pos += 6 }
407 | else { pos += 3 }
408 |
409 | // ISO-8859-1 is the default encoding
410 | var encoding = CFStringBuiltInEncodings.isoLatin1.rawValue
411 | var byteOrderMark = false
412 |
413 | if _tagData[pos] == 3 {
414 | encoding = CFStringBuiltInEncodings.UTF8.rawValue
415 | } else if _tagData[pos] == 2 {
416 | encoding = CFStringBuiltInEncodings.UTF16BE.rawValue
417 | } else if _tagData[pos] == 1 {
418 | encoding = CFStringBuiltInEncodings.UTF16.rawValue
419 | byteOrderMark = true
420 | }
421 | let name = String(bytes: frameName, encoding: .utf8) ?? ""
422 | if name == "TIT2" || name == "TT2" {
423 | _title = parseContent(framesize: UInt32(framesize), pos: UInt32(pos) + 1, encoding: encoding, byteOrderMark: byteOrderMark)
424 | id3_log("ID3 title parsed: \(_title)")
425 | } else if name == "TALB" {
426 | _album = parseContent(framesize: UInt32(framesize), pos: UInt32(pos) + 1, encoding: encoding, byteOrderMark: byteOrderMark)
427 | id3_log("ID3 album parsed: \(_album)")
428 | } else if name == "TPE1" || name == "TP1" {
429 | _performer = parseContent(framesize: UInt32(framesize), pos: UInt32(pos) + 1, encoding: encoding, byteOrderMark: byteOrderMark)
430 | id3_log("ID3 performer parsed:\(_performer)")
431 | } else if name == "APIC" {
432 | var dataPos = pos + 1
433 | var imageType: [UInt8] = []
434 | for i in dataPos ..< (dataPos + 65) {
435 | if _tagData[i] != 0 {
436 | imageType.append(_tagData[i])
437 | } else { break }
438 | }
439 | dataPos += imageType.count + 1
440 | let type = String(bytes: imageType, encoding: .utf8) ?? ""
441 | let jpeg = type == "image/jpeg" || type == "image/jpg"
442 | let png = type == "image/png"
443 | if jpeg || png {
444 | // Skip the image description
445 | var startPos = dataPos
446 | let totalCount = _tagData.count - 2
447 | while dataPos < totalCount {
448 | let first = _tagData[dataPos]
449 | let second = _tagData[dataPos + 1]
450 | if jpeg {
451 | if first == 0xFF, second == 0xD8 {
452 | startPos = dataPos
453 | break
454 | }
455 | } else if png, dataPos + 3 < _tagData.count - 1 {
456 | let thrid = _tagData[dataPos + 2]
457 | let forth = _tagData[dataPos + 3]
458 | if first == 0x89, second == 0x50, thrid == 0x4E, forth == 0x47 {
459 | startPos = dataPos
460 | break
461 | }
462 | }
463 | dataPos += 1
464 | }
465 | id3_log("Image type \(type), parsing, dataPos:\(dataPos)")
466 | if _majorVersion == 3 {
467 | // let a = (i << 24)
468 | // let b = (ii << 16)
469 | // let c = (iii << 8)
470 | // framesize = Int( a + b + c + iv)
471 | let array = [i, ii, iii, iv]
472 | framesize = array.toInt(offsetSize: 8)
473 | id3_log("framesize change due to _majorVersion 3 :\(framesize))")
474 | }
475 | let coverArtSize = framesize - (startPos - pos)
476 | id3_log("image size:\(coverArtSize)")
477 | let start = _tagData.startIndex.advanced(by: startPos)
478 | let end = start.advanced(by: coverArtSize)
479 | let d = _tagData.subdata(in: Range(uncheckedBounds: (start, end)))
480 | _coverArt = d
481 | #if DEBUG
482 | let startI = d.startIndex
483 | let endI = d.startIndex.advanced(by: 10)
484 | let prefix = d.subdata(in: Range(uncheckedBounds: (startI, endI))).map { $0 }
485 | id3_log("_coverArt, prefix 10 bit:\(prefix)")
486 | #endif
487 | } else {
488 | id3_log("->|\(type)|<- is an unknown type for image data, skipping")
489 | }
490 | } else {
491 | // Unknown/unhandled frame
492 | id3_log("Unknown/unhandled frame: \(name), size \(framesize)")
493 | }
494 | pos += framesize
495 | }
496 | }
497 |
498 | private func doneParsing() {
499 | // Push out the metadata
500 | if let d = delegate {
501 | var metadataMap = [MetaDataKey: Metadata]()
502 | if _performer.isEmpty == false {
503 | metadataMap[MetaDataKey.artist] = Metadata.text(_performer)
504 | }
505 | if _album.isEmpty == false {
506 | metadataMap[MetaDataKey.album] = Metadata.text(_album)
507 | }
508 | if _title.isEmpty == false {
509 | metadataMap[MetaDataKey.title] = Metadata.text(_title)
510 | }
511 | if let d = _coverArt {
512 | metadataMap[MetaDataKey.cover] = Metadata.data(d)
513 | }
514 | if metadataMap.count > 0 {
515 | DispatchQueue.main.async { d.id3metaDataAvailable(metaData: metadataMap) }
516 | }
517 | }
518 | DispatchQueue.main.async {
519 | self._tagData.removeAll()
520 | self.setState(state: .tagParsed)
521 | self._parsing = false
522 | }
523 | }
524 | }
525 |
526 |
527 | extension Array where Element == UInt32 {
528 | func toInt(offsetSize: Int) -> Int {
529 | let total = count
530 | var totalSize = 0
531 | for i in 0 ..< total {
532 | totalSize += Int(UInt32(self[i]) << (offsetSize * ((total - 1) - i)))
533 | }
534 | return totalSize
535 | }
536 | }
537 |
--------------------------------------------------------------------------------
/Sources/Log.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Log.swift
3 | // FreePlayer
4 | //
5 | // Created by Lincoln Law on 2017/2/22.
6 | // Copyright © 2017年 Lincoln Law. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// FreePlayer 🖨️模块
12 | public struct FPLogger {
13 |
14 | typealias MessageHandler = (String) -> Void
15 | public static var shared = FPLogger()
16 | /// 当前的记录文件
17 | public private(set) var logfile: String
18 | /// 是否记录到文件,默认开启
19 | public var logToFile: Bool = false { didSet { toggleEnableLog() } }
20 | /// 记录文件的目录
21 | public private(set) var logFolder: String
22 | private var _modules = Set()
23 | private var _date = ""
24 | private var _fileHandler: FileHandle?
25 | private var _logQueue = DispatchQueue(label: "com.SelfStudio.Freeplayer.logger")
26 | private var _openTime = ""
27 | var lastRead = UInt64()
28 | var totalSize = UInt64()
29 |
30 | static let lineSeperator = "\n\n\n"
31 |
32 | private init() {
33 | if let ver = Bundle(for: AudioStream.self).infoDictionary?["CFBundleShortVersionString"] as? String, let version = Double(ver) {
34 | FreePlayerVersion = version
35 | }
36 | let cache = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!
37 | let dir = (cache as NSString).appendingPathComponent("FreePlayer")
38 | let fm = FileManager.default
39 | if !fm.fileExists(atPath: dir) {
40 | try? fm.createDirectory(atPath: dir, withIntermediateDirectories: false, attributes: nil)
41 | }
42 | logFolder = dir
43 | let date = Date()
44 | let fmt = DateFormatter()
45 | fmt.dateFormat = "yyyy-MM-dd HH:mm:ss"
46 | fmt.locale = Locale.current
47 | let total = fmt.string(from: date).components(separatedBy: " ")
48 | _date = total.first ?? ""
49 | _openTime = total.last ?? ""
50 | logfile = dir + "/\(_date).log"
51 | logToFile = UserDefaults.standard.bool(forKey: "FreePlayer.LogToFile")
52 | toggleEnableLog()
53 | DispatchQueue.global(qos: .utility).setTarget(queue: _logQueue)
54 | #if !os(OSX)
55 | NotificationCenter.default.addObserver(forName: NSNotification.Name.UIApplicationWillTerminate, object: nil, queue: OperationQueue.main) { (_) in
56 | // 崩溃前保存记录
57 | FPLogger.shared.logToFile = false
58 | }
59 | #endif
60 | }
61 | /// 开启打印
62 | ///
63 | /// 默认全部开启
64 | /// - Parameter modules: 需要开启的模块
65 | public static func enable(modules: Set = Module.All) {
66 | for item in modules { shared._modules.insert(item) }
67 | }
68 | /// 关闭打印
69 | ///
70 | /// 默认全部关闭
71 | /// - Parameter modules: 需要关闭的模块
72 | public static func disable(modules: Set = Module.All) {
73 | for item in modules { shared._modules.remove(item) }
74 | }
75 | /// 清理记录文件
76 | public static func cleanAllLog() {
77 | let needStopLogging = FPLogger.shared.logToFile
78 | if needStopLogging {
79 | FPLogger.shared.logToFile = false
80 | }
81 | let fm = FileManager.default
82 | let dir = shared.logFolder
83 | try? fm.removeItem(atPath: dir)
84 | if !fm.fileExists(atPath: dir) {
85 | try? fm.createDirectory(atPath: dir, withIntermediateDirectories: false, attributes: nil)
86 | }
87 | if needStopLogging { FPLogger.shared.logToFile = true }
88 | }
89 |
90 | public enum Module {
91 | case audioQueue, audioStream, streamProvider, freePlayer, id3Parser
92 | var symbolize: String {
93 | switch self {
94 | case .audioQueue: return "🌈"
95 | case .audioStream: return "☀️"
96 | case .streamProvider: return "🎙"
97 | case .freePlayer: return "🍄"
98 | case .id3Parser: return "⚡️"
99 | }
100 | }
101 |
102 | public static var All: Set { return [.audioQueue, .audioStream, .streamProvider, .freePlayer, .id3Parser] }
103 |
104 | private static var _lastMessage: String?
105 | private static var _printQueue = DispatchQueue(label: "FPLogger.print")
106 | func log(msg: String, method: String = #function) {
107 | if Module._lastMessage == msg { return }
108 | Module._lastMessage = msg
109 | let total = "\(self.symbolize)\(method):\(msg)"
110 | // #if (arch(i386) || arch(x86_64)) && os(iOS)//iPhone Simulator
111 | FPLogger.Module._printQueue.sync { print(total) }
112 | // #endif
113 | FPLogger.write(msg: total)
114 | }
115 |
116 | func condition(_ condition: Bool, message: String = "", method: String = #function) {
117 | if shared._modules.isEmpty || shared._modules.contains(self) == false { return }
118 | if condition == false { FPLogger.write(msg: method + ":" + message) }
119 | assert(condition, message)
120 | }
121 | }
122 |
123 | public mutating func save() {
124 | DispatchQueue.global(qos: .utility).async {
125 | if FPLogger.shared.logToFile == false { return }
126 | let fs = FileManager.default
127 | if fs.fileExists(atPath: FPLogger.shared.logfile) == false {
128 | FPLogger.shared.lastRead = 0
129 | FPLogger.shared.logToFile = false
130 | FPLogger.shared.logToFile = true
131 | } else {
132 | FPLogger.shared.updateTime()
133 | FPLogger.shared.lastRead = FPLogger.shared.totalSize
134 | FPLogger.write(msg: "🎹:Freeplayer[\(FreePlayerVersion)]@\(FPLogger.shared._openTime)")
135 | }
136 | }
137 | }
138 |
139 | private mutating func updateTime() {
140 | let fmt = DateFormatter()
141 | fmt.dateFormat = "HH:mm:ss"
142 | fmt.locale = Locale.current
143 | let total = fmt.string(from: Date()).components(separatedBy: " ")
144 | _openTime = total.last ?? ""
145 | }
146 | private mutating func toggleEnableLog() {
147 | if logToFile {
148 | updateTime()
149 | DispatchQueue.global(qos: .utility).async {
150 | UserDefaults.standard.set(FPLogger.shared.logToFile, forKey: "FreePlayer.LogToFile")
151 | UserDefaults.standard.synchronize()
152 | }
153 | let u = URL(fileURLWithPath: logfile)
154 | if access(logfile.withCString({$0}), F_OK) == -1 { // file not exists
155 | FileManager.default.createFile(atPath: logfile, contents: nil, attributes: nil)
156 | }
157 | _fileHandler = try? FileHandle(forWritingTo: u)
158 | if let fileHandle = _fileHandler {
159 | lastRead = fileHandle.seekToEndOfFile()
160 | }
161 | totalSize = lastRead
162 | let msg = "🎹:Freeplayer[\(FreePlayerVersion)]@\(_openTime)"
163 | if let fileHandle = _fileHandler {
164 | _logQueue.sync {
165 | guard let data = msg.data(using: String.Encoding.utf8) else { return }
166 | totalSize += UInt64(data.count)
167 | fileHandle.write(data)
168 | }
169 | }
170 | } else {
171 | guard let fileHandle = _fileHandler else { return }
172 | _logQueue.sync { fileHandle.closeFile() }
173 | }
174 | }
175 |
176 | static func write(msg: String) {
177 | guard let fileHandler = shared._fileHandler else { return }
178 | let total = msg + FPLogger.lineSeperator
179 | shared._logQueue.async {
180 | guard let data = total.data(using: .utf8) else { return }
181 | shared.totalSize += UInt64(data.count)
182 | fileHandler.write(data)
183 | }
184 | }
185 | }
186 |
187 | func as_log(_ msg: String, function: String = #function) {
188 | FPLogger.Module.audioStream.log(msg: msg, method: function)
189 | }
190 |
191 | func sp_log(_ msg: String, function: String = #function) {
192 | FPLogger.Module.streamProvider.log(msg: msg, method: function)
193 | }
194 |
195 | func aq_log(_ msg: String, method: String = #function) {
196 | FPLogger.Module.audioQueue.log(msg: msg, method: method)
197 | }
198 |
199 | func aq_assert(_ condition: Bool, message: String = "", method: String = #function) {
200 | FPLogger.Module.audioQueue.condition(condition, message: message, method: method)
201 | }
202 |
203 | func fp_log(_ msg: String, method: String = #function) {
204 | FPLogger.Module.freePlayer.log(msg: msg, method: method)
205 | }
206 |
207 | func id3_log(_ msg: String, method: String = #function) {
208 | FPLogger.Module.id3Parser.log(msg: msg, method: method)
209 | }
210 |
211 |
212 | func debug_log(_ msg: Any...) {
213 | #if DEBUG || ((arch(i386) || arch(x86_64)) && os(iOS))
214 | print(msg)
215 | #endif
216 | }
217 |
218 |
219 | func log_pointer(data: UnsafePointer, len: UInt32) {
220 | var array = [UInt8]()
221 | let l = Int(len)
222 | for i in 0.. ()
54 | public typealias NetworkUnreachable = (Reachability) -> ()
55 |
56 | public enum NetworkStatus: CustomStringConvertible {
57 |
58 | case notReachable, reachableViaWiFi, reachableViaWWAN
59 |
60 | public var description: String {
61 | switch self {
62 | case .reachableViaWWAN: return "Cellular"
63 | case .reachableViaWiFi: return "WiFi"
64 | case .notReachable: return "No Connection"
65 | }
66 | }
67 | }
68 |
69 | public var whenReachable: NetworkReachable?
70 | public var whenUnreachable: NetworkUnreachable?
71 | public var reachableOnWWAN: Bool
72 |
73 | // The notification center on which "reachability changed" events are being posted
74 | public var notificationCenter: NotificationCenter = NotificationCenter.default
75 |
76 | public var currentReachabilityString: String {
77 | return "\(currentReachabilityStatus)"
78 | }
79 |
80 | public var currentReachabilityStatus: NetworkStatus {
81 | guard isReachable else { return .notReachable }
82 |
83 | if isReachableViaWiFi {
84 | return .reachableViaWiFi
85 | }
86 | if isRunningOnDevice {
87 | return .reachableViaWWAN
88 | }
89 |
90 | return .notReachable
91 | }
92 |
93 | private var previousFlags: SCNetworkReachabilityFlags?
94 |
95 | private var isRunningOnDevice: Bool = {
96 | #if (arch(i386) || arch(x86_64)) && os(iOS)
97 | return false
98 | #else
99 | return true
100 | #endif
101 | }()
102 |
103 | private var notifierRunning = false
104 | private var reachabilityRef: SCNetworkReachability?
105 |
106 | private let reachabilitySerialQueue = DispatchQueue(label: "uk.co.ashleymills.reachability")
107 |
108 | required public init(reachabilityRef: SCNetworkReachability) {
109 | reachableOnWWAN = true
110 | self.reachabilityRef = reachabilityRef
111 | }
112 |
113 | public convenience init?(hostname: String) {
114 |
115 | guard let ref = SCNetworkReachabilityCreateWithName(nil, hostname) else { return nil }
116 |
117 | self.init(reachabilityRef: ref)
118 | }
119 |
120 | public convenience init?() {
121 |
122 | var zeroAddress = sockaddr()
123 | zeroAddress.sa_len = UInt8(MemoryLayout.size)
124 | zeroAddress.sa_family = sa_family_t(AF_INET)
125 |
126 | guard let ref: SCNetworkReachability = withUnsafePointer(to: &zeroAddress, {
127 | SCNetworkReachabilityCreateWithAddress(nil, UnsafePointer($0))
128 | }) else { return nil }
129 |
130 | self.init(reachabilityRef: ref)
131 | }
132 |
133 | deinit {
134 | stopNotifier()
135 |
136 | reachabilityRef = nil
137 | whenReachable = nil
138 | whenUnreachable = nil
139 | }
140 | }
141 |
142 | public extension Reachability {
143 |
144 | // MARK: - *** Notifier methods ***
145 | func startNotifier() throws {
146 |
147 | guard let reachabilityRef = reachabilityRef, !notifierRunning else { return }
148 |
149 | var context = SCNetworkReachabilityContext(version: 0, info: nil, retain: nil, release: nil, copyDescription: nil)
150 | context.info = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
151 | if !SCNetworkReachabilitySetCallback(reachabilityRef, callback, &context) {
152 | stopNotifier()
153 | throw ReachabilityError.UnableToSetCallback
154 | }
155 |
156 | if !SCNetworkReachabilitySetDispatchQueue(reachabilityRef, reachabilitySerialQueue) {
157 | stopNotifier()
158 | throw ReachabilityError.UnableToSetDispatchQueue
159 | }
160 |
161 | // Perform an intial check
162 | reachabilitySerialQueue.async {
163 | self.reachabilityChanged()
164 | }
165 |
166 | notifierRunning = true
167 | }
168 |
169 | func stopNotifier() {
170 | defer { notifierRunning = false }
171 | guard let reachabilityRef = reachabilityRef else { return }
172 |
173 | SCNetworkReachabilitySetCallback(reachabilityRef, nil, nil)
174 | SCNetworkReachabilitySetDispatchQueue(reachabilityRef, nil)
175 | }
176 |
177 | // MARK: - *** Connection test methods ***
178 | var isReachable: Bool {
179 |
180 | guard isReachableFlagSet else { return false }
181 |
182 | if isConnectionRequiredAndTransientFlagSet {
183 | return false
184 | }
185 |
186 | if isRunningOnDevice {
187 | if isOnWWANFlagSet && !reachableOnWWAN {
188 | // We don't want to connect when on 3G.
189 | return false
190 | }
191 | }
192 |
193 | return true
194 | }
195 |
196 | var isReachableViaWWAN: Bool {
197 | // Check we're not on the simulator, we're REACHABLE and check we're on WWAN
198 | return isRunningOnDevice && isReachableFlagSet && isOnWWANFlagSet
199 | }
200 |
201 | var isReachableViaWiFi: Bool {
202 |
203 | // Check we're reachable
204 | guard isReachableFlagSet else { return false }
205 |
206 | // If reachable we're reachable, but not on an iOS device (i.e. simulator), we must be on WiFi
207 | guard isRunningOnDevice else { return true }
208 |
209 | // Check we're NOT on WWAN
210 | return !isOnWWANFlagSet
211 | }
212 |
213 | var description: String {
214 |
215 | let W = isRunningOnDevice ? (isOnWWANFlagSet ? "W" : "-") : "X"
216 | let R = isReachableFlagSet ? "R" : "-"
217 | let c = isConnectionRequiredFlagSet ? "c" : "-"
218 | let t = isTransientConnectionFlagSet ? "t" : "-"
219 | let i = isInterventionRequiredFlagSet ? "i" : "-"
220 | let C = isConnectionOnTrafficFlagSet ? "C" : "-"
221 | let D = isConnectionOnDemandFlagSet ? "D" : "-"
222 | let l = isLocalAddressFlagSet ? "l" : "-"
223 | let d = isDirectFlagSet ? "d" : "-"
224 |
225 | return "\(W)\(R) \(c)\(t)\(i)\(C)\(D)\(l)\(d)"
226 | }
227 | }
228 |
229 | private extension Reachability {
230 |
231 | func reachabilityChanged() {
232 |
233 | let flags = reachabilityFlags
234 |
235 | guard previousFlags != flags else { return }
236 |
237 | let block = isReachable ? whenReachable : whenUnreachable
238 | block?(self)
239 |
240 | self.notificationCenter.post(name: ReachabilityChangedNotification, object:self)
241 |
242 | previousFlags = flags
243 | }
244 |
245 | var isOnWWANFlagSet: Bool {
246 | #if os(iOS)
247 | return reachabilityFlags.contains(.isWWAN)
248 | #else
249 | return false
250 | #endif
251 | }
252 | var isReachableFlagSet: Bool {
253 | return reachabilityFlags.contains(.reachable)
254 | }
255 | var isConnectionRequiredFlagSet: Bool {
256 | return reachabilityFlags.contains(.connectionRequired)
257 | }
258 | var isInterventionRequiredFlagSet: Bool {
259 | return reachabilityFlags.contains(.interventionRequired)
260 | }
261 | var isConnectionOnTrafficFlagSet: Bool {
262 | return reachabilityFlags.contains(.connectionOnTraffic)
263 | }
264 | var isConnectionOnDemandFlagSet: Bool {
265 | return reachabilityFlags.contains(.connectionOnDemand)
266 | }
267 | var isConnectionOnTrafficOrDemandFlagSet: Bool {
268 | return !reachabilityFlags.intersection([.connectionOnTraffic, .connectionOnDemand]).isEmpty
269 | }
270 | var isTransientConnectionFlagSet: Bool {
271 | return reachabilityFlags.contains(.transientConnection)
272 | }
273 | var isLocalAddressFlagSet: Bool {
274 | return reachabilityFlags.contains(.isLocalAddress)
275 | }
276 | var isDirectFlagSet: Bool {
277 | return reachabilityFlags.contains(.isDirect)
278 | }
279 | var isConnectionRequiredAndTransientFlagSet: Bool {
280 | return reachabilityFlags.intersection([.connectionRequired, .transientConnection]) == [.connectionRequired, .transientConnection]
281 | }
282 |
283 | var reachabilityFlags: SCNetworkReachabilityFlags {
284 |
285 | guard let reachabilityRef = reachabilityRef else { return SCNetworkReachabilityFlags() }
286 |
287 | var flags = SCNetworkReachabilityFlags()
288 | let gotFlags = withUnsafeMutablePointer(to: &flags) {
289 | SCNetworkReachabilityGetFlags(reachabilityRef, UnsafeMutablePointer($0))
290 | }
291 |
292 | if gotFlags {
293 | return flags
294 | } else {
295 | return SCNetworkReachabilityFlags()
296 | }
297 | }
298 | }
299 |
--------------------------------------------------------------------------------
/Sources/RunloopQueue.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RunloopQueue.swift
3 | // RunloopQueue
4 | //
5 | // Created by Daniel Kennett on 2017-02-14.
6 | // For license information, see LICENSE.md.
7 | //
8 |
9 | import Foundation
10 |
11 | /// RunloopQueue is a serial queue based on CFRunLoop, running on the background thread.
12 | public final class RunloopQueue {
13 |
14 | //MARK: - Code That Runs On The Main/Creating Thread
15 | private let thread: RunloopQueueThread
16 |
17 | /// Init a new queue with the given name.
18 | ///
19 | /// - Parameter name: The name of the queue.
20 | public init(named name: String?) {
21 | thread = RunloopQueueThread()
22 | thread.name = name
23 | startRunloop()
24 | }
25 |
26 | deinit {
27 | let runloop = self.runloop
28 | sync { CFRunLoopStop(runloop) }
29 | print("RunloopQueue deinit")
30 | }
31 |
32 | /// Returns `true` if the queue is running, otherwise `false`. Once stopped, a queue cannot be restarted.
33 | public var running: Bool { return true }
34 |
35 |
36 | /// Execute a block of code in an asynchronous manner. Will return immediately.
37 | ///
38 | /// - Parameter block: The block of code to execute.
39 | public func async(_ block: @escaping () -> Void) {
40 | CFRunLoopPerformBlock(runloop, CFRunLoopMode.defaultMode.rawValue, block)
41 | thread.awake()
42 | }
43 |
44 | /// Execute a block of code in a synchronous manner. Will return when the code has executed.
45 | ///
46 | /// It's important to be careful with `sync()` to avoid deadlocks. In particular, calling `sync()` from inside
47 | /// a block previously passed to `sync()` will deadlock if the second call is made from a different thread.
48 | ///
49 | /// - Parameter block: The block of code to execute.
50 | public func sync(_ block: @escaping () -> Void) {
51 |
52 | if isRunningOnQueue() {
53 | block()
54 | return
55 | }
56 |
57 | let conditionLock = NSConditionLock(condition: 0)
58 |
59 | CFRunLoopPerformBlock(runloop, CFRunLoopMode.defaultMode.rawValue) {
60 | conditionLock.lock()
61 | block()
62 | conditionLock.unlock(withCondition: 1)
63 | }
64 |
65 | thread.awake()
66 | conditionLock.lock(whenCondition: 1)
67 | conditionLock.unlock()
68 | }
69 |
70 | /// Query if the caller is running on this queue.
71 | ///
72 | /// - Returns: `true` if the caller is running on this queue, otherwise `false`.
73 | public func isRunningOnQueue() -> Bool {
74 | return CFEqual(CFRunLoopGetCurrent(), runloop)
75 | }
76 |
77 | //MARK: - Code That Runs On The Background Thread
78 |
79 | private var runloop: CFRunLoop! = nil
80 | private func startRunloop() {
81 |
82 | let conditionLock = NSConditionLock(condition: 0)
83 |
84 | thread.start() {
85 | [weak self] runloop in
86 | // This is on the background thread.
87 |
88 | conditionLock.lock()
89 | defer { conditionLock.unlock(withCondition: 1) }
90 |
91 | guard let `self` = self else { return }
92 | self.runloop = runloop
93 | }
94 |
95 | conditionLock.lock(whenCondition: 1)
96 | conditionLock.unlock()
97 | }
98 | }
99 |
100 | private class RunloopQueueThread: Thread {
101 |
102 | // Required to keep the runloop running when nothing is going on.
103 | private let runloopSource: CFRunLoopSource
104 | private var currentRunloop: CFRunLoop?
105 |
106 | override init() {
107 | var sourceContext = CFRunLoopSourceContext()
108 | runloopSource = CFRunLoopSourceCreate(nil, 0, &sourceContext)
109 | }
110 |
111 | /// The callback to be called once the runloop has started executing. Will be called on the runloop's own thread.
112 | var whenReadyCallback: ((CFRunLoop) -> Void)? = nil
113 |
114 | func start(whenReady call: @escaping (CFRunLoop) -> Void) {
115 | whenReadyCallback = call
116 | start()
117 | }
118 |
119 | func awake() {
120 | guard let runloop = currentRunloop else { return }
121 | if CFRunLoopIsWaiting(runloop) {
122 | CFRunLoopSourceSignal(runloopSource)
123 | CFRunLoopWakeUp(runloop)
124 | }
125 | }
126 |
127 | override func main() {
128 |
129 | let strongSelf = self
130 | let runloop = CFRunLoopGetCurrent()!
131 | currentRunloop = runloop
132 |
133 | CFRunLoopAddSource(runloop, runloopSource, .commonModes)
134 |
135 | let observer = CFRunLoopObserverCreateWithHandler(nil, CFRunLoopActivity.entry.rawValue, false, 0) {
136 | observer, activity in
137 | strongSelf.whenReadyCallback?(runloop)
138 | }
139 |
140 | CFRunLoopAddObserver(runloop, observer, .commonModes)
141 | CFRunLoopRun()
142 | CFRunLoopRemoveObserver(runloop, observer, .commonModes)
143 | CFRunLoopRemoveSource(runloop, runloopSource, .commonModes)
144 |
145 | currentRunloop = nil
146 | }
147 | }
148 |
149 |
150 | public extension RunloopQueue {
151 |
152 | /// Schedules the given stream into the queue.
153 | ///
154 | /// - Parameter stream: The stream to schedule.
155 |
156 | public func schedule(_ stream: Stream) {
157 | if let input = stream as? InputStream {
158 | sync { CFReadStreamScheduleWithRunLoop(input as CFReadStream, CFRunLoopGetCurrent(), .commonModes) }
159 | } else if let output = stream as? OutputStream {
160 | sync { CFWriteStreamScheduleWithRunLoop(output as CFWriteStream, CFRunLoopGetCurrent(), .commonModes) }
161 | }
162 | }
163 |
164 | /// Removes the given stream from the queue.
165 | ///
166 | /// - Parameter stream: The stream to remove.
167 |
168 | public func unschedule(_ stream: Stream) {
169 | if let input = stream as? InputStream {
170 | sync { CFReadStreamUnscheduleFromRunLoop(input as CFReadStream, CFRunLoopGetCurrent(), .commonModes) }
171 | } else if let output = stream as? OutputStream {
172 | sync { CFWriteStreamUnscheduleFromRunLoop(output as CFWriteStream, CFRunLoopGetCurrent(), .commonModes) }
173 | }
174 | }
175 |
176 | public func addTimer(_ value: CFRunLoopTimer) {
177 | CFRunLoopAddTimer(CFRunLoopGetCurrent(), value, .commonModes)
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/Sources/StreamConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StreamConfiguration.swift
3 | // FreePlayer
4 | //
5 | // Created by Lincoln Law on 2017/2/19.
6 | // Copyright © 2017年 Lincoln Law. All rights reserved.
7 | //
8 |
9 | import AudioToolbox
10 | import AVFoundation
11 | #if os(iOS)
12 | import UIKit
13 | #endif
14 | public var FreePlayerVersion: Double = 2.0
15 | /** FreePlayer 配置单例 */
16 | /**
17 | # 因为 StreamConfiguration.shared 是 struct
18 | # 所以,不能这样赋值(会触发 copy on write)
19 | var shared = StreamConfiguration.shared
20 | shared.xxx = xxx
21 | */
22 | public struct StreamConfiguration {
23 |
24 | public enum AuthenticationScheme {
25 |
26 | case digest, basic
27 |
28 | var name: CFString {
29 | switch self {
30 | case .digest: return kCFHTTPAuthenticationSchemeDigest
31 | case .basic: return kCFHTTPAuthenticationSchemeBasic
32 | }
33 | }
34 | }
35 |
36 | public static var shared: StreamConfiguration = StreamConfiguration()
37 | /** 缓冲数 */
38 | public var bufferCount = UInt()
39 | /** 每个缓冲的大小 */
40 | public var bufferSize = UInt()
41 | /** 最大帧数 */
42 | public var maxPacketDescs = UInt()
43 | /** http 缓冲大小 */
44 | public var httpConnectionBufferSize = UInt()
45 | /** 转化为 PCM 的采样率 */
46 | public var outputSampleRate = Double()
47 | /** 转化为 PCM 声道数 */
48 | public var outputNumChannels = Int()
49 | ///
50 | public var bounceInterval = Int()
51 | /** 监控播放最大时长 ,超过时长则🚔*/
52 | public var startupWatchdogPeriod = Int()
53 | ///
54 | public var maxBounceCount = Int()
55 | /** 磁盘最大缓存数(bytes)*/
56 | public var maxDiskCacheSize = Int()
57 | /** 最大缓冲数(bytes) */
58 | public var maxPrebufferedByteCount = Int()
59 | /** 流媒体最低预缓冲数(bytes)*/
60 | public var requiredInitialPrebufferedByteCountForContinuousStream = Int()
61 | /** 非流媒体最低预缓冲数(bytes)*/
62 | public var requiredInitialPrebufferedByteCountForNonContinuousStream = Int()
63 | /** 最低预缓冲秒数 */
64 | public var requiredPrebufferSizeInSeconds = Int()
65 | /** 最低预缓冲帧数 */
66 | public var requiredInitialPrebufferedPacketCount = Int()
67 | /** 自定义 UA */
68 | public var userAgent: String?
69 | /** 缓存目录 */
70 | public lazy var cacheDirectory: String = {
71 | let base = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first!
72 | let target = "\(base)/FreePlayer/Tmp"
73 | let fs = FileManager.default
74 | guard fs.fileExists(atPath: target) == true else { return target }
75 | try? fs.createDirectory(atPath: target, withIntermediateDirectories: true, attributes: nil)
76 | return target
77 | }()
78 | /** 缓存命名策略 */
79 | public lazy var cacheNaming: CacheNamingPolicy = .custom({ (url) -> String in
80 | let raw = url.path
81 | guard let dat = raw.data(using: .utf8) else { return raw }
82 | let sub = dat.base64EncodedString()
83 | let range = NSMakeRange(0, min(24, sub.count))
84 | let value = String(sub[Range(range, in: sub)!])
85 | return value//"\(value).dou"
86 | })
87 | /** 缓存策略 */
88 | public lazy var cachePolicy: CachePolicy = .enable
89 | /** 代理策略 */
90 | public lazy var proxyPolicy: ProxyPolicy = .system
91 | /** 自定义 http header 字典 */
92 | public var predefinedHttpHeaderValues: [String : String] = [:]
93 | /** 使用时间数计算预缓冲大小 */
94 | public var usePrebufferSizeCalculationInSeconds = Bool()
95 | /** 使用帧数计算预缓冲大小 */
96 | public var usePrebufferSizeCalculationInPackets = Bool()
97 | /** 缓存播放文件 */
98 | public var cacheEnabled = false
99 | /** 使用缓存 seeking */
100 | public var seekingFromCacheEnabled = false
101 | /** 自动控制 AudioSession */
102 | public var automaticAudioSessionHandlingEnabled = false
103 | /** 开启 Time And Pitch Conversion */
104 | public var enableTimeAndPitchConversion = false
105 | /** 需要内容类型检查 */
106 | public var requireStrictContentTypeChecking = false
107 | /** 远程连接最大重试次数 */
108 | public var maxRemoteStreamOpenRetry = 5
109 | #if !os(OSX)
110 | /** 需要网络播放检查 */
111 | public var requireNetworkPermision = true
112 | #endif
113 |
114 | /** 自动填充ID3的信息到 NowPlayingCenter */
115 | public var autoFillID3InfoToNowPlayingCenter = false
116 | /** 使用自定义代理 */
117 | public var usingCustomProxy = false { didSet { didConfigureProxy() } }
118 | /** 使用自定义代理 用户名*/
119 | public var customProxyUsername = ""
120 | /** 使用自定义代理 密码*/
121 | public var customProxyPassword = ""
122 | /** 使用自定义代理 Http Host */
123 | public var customProxyHttpHost = "" { didSet { didConfigureProxy() } }
124 | /** 使用自定义代理 Http Port */
125 | public var customProxyHttpPort = 0 { didSet { didConfigureProxy() } }
126 | /** 使用自定义代理 authenticationScheme, kCFHTTPAuthenticationSchemeBasic... */
127 | public var customProxyAuthenticationScheme: AuthenticationScheme = .digest { didSet { didConfigureProxy() } }
128 |
129 |
130 | public var enableVolumeMixer = false
131 |
132 | public var equalizerBandFrequencies: [Float] = []
133 |
134 |
135 |
136 | private init() {
137 | // https://github.com/muhku/FreeStreamer/issues/387
138 | bufferCount = 64
139 | bufferSize = 8192
140 | maxPacketDescs = 512
141 | httpConnectionBufferSize = 8192
142 | outputSampleRate = 44100
143 | outputNumChannels = 2
144 | bounceInterval = 10
145 | maxBounceCount = 4 // Max number of bufferings in bounceInterval seconds
146 | startupWatchdogPeriod = 30
147 |
148 | /* Adjust the max in-memory cache to 20 MB with newer 64 bit devices or 5 MB for 32 bit devices*/
149 | // #if DEBUG
150 | // maxPrebufferedByteCount = 1000000
151 | // #else
152 | maxPrebufferedByteCount = (MemoryLayout.size == 8) ? 20000000 : 5000000
153 | // #endif
154 |
155 | cacheEnabled = true
156 | seekingFromCacheEnabled = false
157 | automaticAudioSessionHandlingEnabled = true
158 | enableTimeAndPitchConversion = false
159 | requireStrictContentTypeChecking = true
160 | maxDiskCacheSize = 256000000 // 256 MB
161 | usePrebufferSizeCalculationInSeconds = true
162 | usePrebufferSizeCalculationInPackets = false
163 | requiredInitialPrebufferedPacketCount = 32
164 | requiredPrebufferSizeInSeconds = 7
165 | // With dynamic calculation, these are actually the maximum sizes, the dynamic
166 | // calculation may lower the sizes based on the stream bitrate
167 | requiredInitialPrebufferedByteCountForContinuousStream = 256000
168 | requiredInitialPrebufferedByteCountForNonContinuousStream = 256000
169 |
170 | var osStr = ""
171 | #if os(iOS)
172 | let session = AVAudioSession.sharedInstance()
173 | let sampleRate = session.sampleRate
174 | if sampleRate > 0 { outputSampleRate = sampleRate }
175 | let channels = session.outputNumberOfChannels
176 | if channels > 0 { outputNumChannels = channels }
177 | let version = UIDevice.current.systemVersion
178 | osStr = "iOS \(version)"
179 | #elseif os(OSX)
180 | requiredPrebufferSizeInSeconds = 3
181 | // No need to be so concervative with the cache sizes
182 | maxPrebufferedByteCount = 16000000 // 16 MB
183 | osStr = "macOS"
184 | #endif
185 | userAgent = "FreePlayer/\(FreePlayerVersion) \(osStr)"
186 | }
187 |
188 | private func didConfigureProxy() {
189 | NowPlayingInfo.shared.updateProxy()
190 | }
191 | }
192 | extension StreamConfiguration {
193 |
194 | public enum State { case idle, running, paused, unknown }
195 |
196 | public enum Metadata {
197 | case text(String)
198 | case data(Data)
199 | case other(String, String)
200 | }
201 | public enum MetaDataKey: String {
202 | case artist = "MPMediaItemPropertyArtist"
203 | case title = "MPMediaItemPropertyTitle"
204 | case cover = "CoverArt"
205 | case album
206 | case other
207 | }
208 | public enum CacheNamingPolicy {
209 | /// default is url.path.hashValue
210 | case `default`
211 | case custom((URL) -> String)
212 |
213 | func name(for url: URL) -> String {
214 | switch self {
215 | case .default: return url.path.replacingOccurrences(of: "/", with: "_")
216 | case .custom(let block): return block(url)
217 | }
218 | }
219 | }
220 |
221 | public enum CachePolicy {
222 | case enable
223 | case disable
224 | case enableAndSearching(String)
225 |
226 | var isEnabled: Bool {
227 | switch self {
228 | case .disable: return false
229 | default: return true
230 | }
231 | }
232 |
233 | var additionalFolder: String? {
234 | switch self {
235 | case .enableAndSearching(let folder): return folder
236 | default: return nil
237 | }
238 | }
239 | }
240 |
241 | public enum ProxyPolicy {
242 | case system
243 | case custom(Info)
244 |
245 | public struct Info {
246 | /** 使用自定义代理 用户名 */
247 | public let username: String
248 | /** 使用自定义代理 密码 */
249 | public let password: String
250 | /** 使用自定义代理 Http Host */
251 | public let host: String
252 | /** 使用自定义代理 Http Port */
253 | public let port: UInt
254 | /** 使用自定义代理 authenticationScheme, kCFHTTPAuthenticationSchemeBasic... */
255 | public let scheme: AuthenticationScheme
256 |
257 | public init(username: String, password: String, host: String, port: UInt, scheme: AuthenticationScheme) {
258 | self.username = username
259 | self.password = password
260 | self.host = host
261 | self.port = port
262 | self.scheme = scheme
263 | }
264 |
265 | public enum AuthenticationScheme {
266 | case digest, basic
267 | var name: CFString {
268 | switch self {
269 | case .digest: return kCFHTTPAuthenticationSchemeDigest
270 | case .basic: return kCFHTTPAuthenticationSchemeBasic
271 | }
272 | }
273 | }
274 | }
275 | }
276 | }
277 | public typealias State = StreamConfiguration.State
278 | public typealias MetaDataKey = StreamConfiguration.MetaDataKey
279 | public typealias Metadata = StreamConfiguration.Metadata
280 |
--------------------------------------------------------------------------------
/Sources/StreamInput.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InputStreamProtocol.swift
3 | // FreePlayer
4 | //
5 | // Created by Lincoln Law on 2017/2/19.
6 | // Copyright © 2017年 Lincoln Law. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | protocol StreamInputDelegate: class {
12 | func streamIsReadyRead()
13 | func streamHasBytesAvailable(data: UnsafePointer, numBytes: UInt32)
14 | func streamEndEncountered()
15 | func streamErrorOccurred(errorDesc: String)
16 | func streamMetaDataAvailable(metaData: [String: Metadata])
17 | func streamMetaDataByteSizeAvailable(sizeInBytes: UInt32)
18 | func streamHasDataCanPlay() -> Bool
19 | }
20 |
21 | protocol StreamInputProtocol {
22 | weak var delegate: StreamInputDelegate? { get set }
23 | var position: Position { get }
24 | var contentType: String { get }
25 | var contentLength: UInt64 { get }
26 | var errorDescription: String? { get }
27 |
28 | @discardableResult func open(_ position: Position) -> Bool
29 | @discardableResult func open() -> Bool
30 | func close()
31 | func setScheduledInRunLoop(run: Bool)
32 | func set(url: URL)
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/StreamOutput.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FileOutPutProtocol.swift
3 | // FreePlayer
4 | //
5 | // Created by Lincoln Law on 2017/2/19.
6 | // Copyright © 2017年 Lincoln Law. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | /// 数据存储管理
12 | final class StreamOutputManager {
13 |
14 | private var _writeStream: CFWriteStream?
15 |
16 | deinit {
17 | guard let stream = _writeStream else { return }
18 | CFWriteStreamClose(stream)
19 | }
20 |
21 | init(fileURL: URL) {
22 | do {
23 | if FileManager.default.fileExists(atPath: fileURL.absoluteString) {
24 | cs_log("incomplete file at:\(fileURL), removing...")
25 | try FileManager.default.removeItem(at: fileURL)
26 | }
27 | cs_log("remove file:\(fileURL) success")
28 | let stream = CFWriteStreamCreateWithFile(kCFAllocatorDefault, fileURL as CFURL)
29 | CFWriteStreamOpen(stream)
30 | self._writeStream = stream
31 | } catch { cs_log("remove incomplete file fail:\(error)") }
32 | }
33 |
34 | @discardableResult func write(data: UnsafePointer, length: Int) -> Bool {
35 | guard let stream = _writeStream else { return false }
36 | let result = CFWriteStreamWrite(stream, data, length)
37 | return result != -1
38 | }
39 | }
40 |
41 |
--------------------------------------------------------------------------------