Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 159627c63e | |||
| 07230cce1a | |||
| a33aee80d1 | |||
| e1d3da1ddb | |||
| 8c2524d990 | |||
| be1b7aa05f | |||
| 4b57fee75c | |||
| fc9c43a23c |
+4
@@ -14,6 +14,7 @@
|
||||
79D8DF73FA7CDD6E266BAE71D46E035F /* Pods-SwiftAudioPlayer_Tests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 50C71346CE708A211A5AFAC20BAE48CB /* Pods-SwiftAudioPlayer_Tests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
831B263D357A5FA2DDC7B1AE4B374092 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A16F4CFC63FAC439D7A04994F579A03 /* Foundation.framework */; };
|
||||
8F93DB166237195ED222EE55B6404625 /* Pods-SwiftAudioPlayer_Example-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B0B76CB1439F4D361322144E5A65C3A /* Pods-SwiftAudioPlayer_Example-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
A40DBE292391D9CA00F86146 /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = A40DBE282391D9C900F86146 /* Data.swift */; };
|
||||
A41AA0D2238BB9B600A467E1 /* SAPlayingStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = A41AA0D1238BB9B600A467E1 /* SAPlayingStatus.swift */; };
|
||||
A4681FC6220113880018AB51 /* SAPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F8D2200E00E0018AB51 /* SAPlayer.swift */; };
|
||||
A4681FC72201138B0018AB51 /* SAPlayerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4681F912200E1950018AB51 /* SAPlayerDelegate.swift */; };
|
||||
@@ -96,6 +97,7 @@
|
||||
99925F09FC9C6EA4B9C0508F4E2D1FE2 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
A19C8F889C787C19BE4123C1896AF501 /* Pods-SwiftAudioPlayer_Example-resources.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-SwiftAudioPlayer_Example-resources.sh"; sourceTree = "<group>"; };
|
||||
A39F2A138CF40C1051CA9E227429A86D /* SwiftAudioPlayer.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = SwiftAudioPlayer.modulemap; sourceTree = "<group>"; };
|
||||
A40DBE282391D9C900F86146 /* Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = "<group>"; };
|
||||
A41AA0D1238BB9B600A467E1 /* SAPlayingStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SAPlayingStatus.swift; sourceTree = "<group>"; };
|
||||
A4523BC8220A0B3C0079C4BC /* Credited_LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = Credited_LICENSE; sourceTree = "<group>"; };
|
||||
A4681F802200D0500018AB51 /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = "<group>"; };
|
||||
@@ -273,6 +275,7 @@
|
||||
A4681F872200DAD50018AB51 /* DirectorThreadSafeClosures.swift */,
|
||||
A4681F892200DB3C0018AB51 /* Date.swift */,
|
||||
A4681F962200E2E20018AB51 /* URL.swift */,
|
||||
A40DBE282391D9C900F86146 /* Data.swift */,
|
||||
);
|
||||
path = Util;
|
||||
sourceTree = "<group>";
|
||||
@@ -524,6 +527,7 @@
|
||||
A4681FD2220113B20018AB51 /* AudioParser.swift in Sources */,
|
||||
A4681FCF220113A40018AB51 /* AudioConverterListener.swift in Sources */,
|
||||
A4681FE1220113E70018AB51 /* Constants.swift in Sources */,
|
||||
A40DBE292391D9CA00F86146 /* Data.swift in Sources */,
|
||||
A4FBA6B5221B74C900D5A353 /* SALockScreenInfo.swift in Sources */,
|
||||
A4681FC6220113880018AB51 /* SAPlayer.swift in Sources */,
|
||||
A4FBA6B7221BAC3D00D5A353 /* SAPlayerUpdateSubscription.swift in Sources */,
|
||||
|
||||
@@ -174,6 +174,10 @@ class ViewController: UIViewController {
|
||||
self.isPlayable = false
|
||||
self.playPauseButton.setTitle("Loading", for: .normal)
|
||||
return
|
||||
case .ended:
|
||||
self.isPlayable = false
|
||||
self.playPauseButton.setTitle("Done", for: .normal)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ iOS 10.0 and higher.
|
||||
|
||||
1. Clone repo
|
||||
2. CD to directory
|
||||
3. run `pod install` in terminal
|
||||
4. Run
|
||||
3. Run `pod install` in terminal
|
||||
4. Build and run
|
||||
|
||||
### Installation
|
||||
|
||||
@@ -252,7 +252,7 @@ Changes in the duration of the current initialized audio. Especially helpful for
|
||||
### PlayingStatus
|
||||
Payload = `SAPlayingStatus`
|
||||
|
||||
Changes in the playing status of the player. Can be one of the following 3: `playing`, `paused`, `buffering`.
|
||||
Changes in the playing status of the player. Can be one of the following 4: `playing`, `paused`, `buffering`, `ended` (audio ended).
|
||||
|
||||
### StreamingBuffer
|
||||
Payload = `SAAudioAvailabilityRange`
|
||||
|
||||
@@ -98,7 +98,7 @@ class AudioDiskEngine: AudioEngine {
|
||||
if state == .resumed {
|
||||
state = .suspended
|
||||
}
|
||||
delegate?.didEndPlaying()
|
||||
playingStatus = .ended
|
||||
}
|
||||
|
||||
guard audioSampleRate != 0 else {
|
||||
|
||||
@@ -27,6 +27,7 @@ import Foundation
|
||||
import AVFoundation
|
||||
|
||||
protocol AudioEngineProtocol {
|
||||
var engine: AVAudioEngine { get set }
|
||||
func play()
|
||||
func pause()
|
||||
func seek(toNeedle needle: Needle)
|
||||
@@ -42,7 +43,7 @@ class AudioEngine: AudioEngineProtocol {
|
||||
weak var delegate:AudioEngineDelegate?
|
||||
let key:Key
|
||||
|
||||
let engine = AVAudioEngine()
|
||||
var engine = AVAudioEngine()
|
||||
let playerNode = AVAudioPlayerNode()
|
||||
|
||||
var timer: Timer?
|
||||
@@ -77,6 +78,10 @@ class AudioEngine: AudioEngineProtocol {
|
||||
return
|
||||
}
|
||||
|
||||
if status == .ended {
|
||||
delegate?.didEndPlaying()
|
||||
}
|
||||
|
||||
AudioClockDirector.shared.audioPlayingStatusWasChanged(key, status: status)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ class AudioStreamEngine: AudioEngine {
|
||||
Log.info("reached end of audio")
|
||||
seek(toNeedle: 0)
|
||||
pause()
|
||||
delegate?.didEndPlaying()
|
||||
playingStatus = .ended
|
||||
}
|
||||
needle = (currentTime + currentTimeOffset)
|
||||
}
|
||||
|
||||
@@ -152,30 +152,34 @@ class AudioThrottler: AudioThrottleable {
|
||||
Log.debug("offset: \(offset) within network packet of range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset) is next sent: \(wrappedNetworkData.isNextSent())")
|
||||
|
||||
if wrappedNetworkData.alreadySent {
|
||||
if !wrappedNetworkData.isNextSent() {
|
||||
var bytesSent: UInt = 0
|
||||
var current = wrappedNetworkData
|
||||
|
||||
// Sometimes the next data packet is smaller than a full audio chunk size, so we need to ensure we send up enough packets for the audio chunk. This prevented Issue #4 where tsreaming would randomly get stuck in a state needing more data up the chain.
|
||||
// https://github.com/tanhakabir/SwiftAudioPlayer/issues/4
|
||||
while bytesSent < largestPollingOffsetDifference {
|
||||
if let next = current.next {
|
||||
Log.debug("Sending next network packet with range: \(next.startOffset) to \(next.endOffset)")
|
||||
Log.debug("already sent offset: \(offset) within network packet of range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)")
|
||||
|
||||
var bytesSent: UInt = 0
|
||||
var current = wrappedNetworkData
|
||||
|
||||
// Sometimes the next data packet is smaller than a full audio chunk size, so we need to ensure we send up enough packets for the audio chunk. This prevented Issue #4 where tsreaming would randomly get stuck in a state needing more data up the chain.
|
||||
// https://github.com/tanhakabir/SwiftAudioPlayer/issues/4
|
||||
while bytesSent < largestPollingOffsetDifference {
|
||||
if let next = current.next {
|
||||
if !next.alreadySent {
|
||||
Log.info("Sending next network packet with range: \(next.startOffset) to \(next.endOffset), have sent \(bytesSent) bytes so far from \(largestPollingOffsetDifference) bytes")
|
||||
next.alreadySent = true
|
||||
delegate?.shouldProcess(networkData: next.data)
|
||||
bytesSent += next.byteCount
|
||||
current = next
|
||||
} else {
|
||||
return
|
||||
}
|
||||
bytesSent += next.byteCount
|
||||
current = next
|
||||
} else {
|
||||
Log.debug("next package doesn't exist, bytes sent so far: \(bytesSent)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
Log.debug("Found network packet to send with range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)")
|
||||
delegate?.shouldProcess(networkData: wrappedNetworkData.data)
|
||||
Log.info("Found network packet to send with range: \(wrappedNetworkData.startOffset) to \(wrappedNetworkData.endOffset)")
|
||||
wrappedNetworkData.alreadySent = true
|
||||
delegate?.shouldProcess(networkData: wrappedNetworkData.data)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func ConverterListener(_ converter: AudioConverterRef, _ packetCount: UnsafeMuta
|
||||
let packetByteCount = packet.count //this is not the count of an array
|
||||
ioData.pointee.mNumberBuffers = 1
|
||||
ioData.pointee.mBuffers.mData = UnsafeMutableRawPointer.allocate(byteCount: packetByteCount, alignment: 0)
|
||||
_ = packet.withUnsafeMutableBytes({ (bytes: UnsafeMutablePointer<UInt8>) in
|
||||
_ = packet.accessMutableBytes({ (bytes: UnsafeMutablePointer<UInt8>) in
|
||||
memcpy((ioData.pointee.mBuffers.mData?.assumingMemoryBound(to: UInt8.self))!, bytes, packetByteCount)
|
||||
})
|
||||
ioData.pointee.mBuffers.mDataByteSize = UInt32(packetByteCount)
|
||||
|
||||
@@ -295,7 +295,7 @@ extension AudioParser: AudioThrottleDelegate {
|
||||
let sID = self.streamID!
|
||||
let dataSize = data.count
|
||||
|
||||
let _ = try data.withUnsafeBytes({ (bytes:UnsafePointer<UInt8>) in
|
||||
_ = try data.accessBytes({ (bytes: UnsafePointer<UInt8>) in
|
||||
let result:OSStatus = AudioFileStreamParseBytes(sID, UInt32(dataSize), bytes, [])
|
||||
guard result == noErr else {
|
||||
Log.monitor(ParserError.failedToParseBytes(result).errorDescription as Any)
|
||||
|
||||
@@ -30,4 +30,5 @@ public enum SAPlayingStatus {
|
||||
case playing
|
||||
case paused
|
||||
case buffering
|
||||
case ended
|
||||
}
|
||||
|
||||
@@ -35,6 +35,10 @@ protocol LockScreenViewProtocol {
|
||||
}
|
||||
|
||||
extension LockScreenViewProtocol {
|
||||
func clearLockScreenInfo() {
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = [:]
|
||||
}
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
func setLockScreenInfo(withMediaInfo info: SALockScreenInfo?, duration: Duration) {
|
||||
var nowPlayingInfo:[String : Any] = [:]
|
||||
|
||||
@@ -35,6 +35,33 @@ public class SAPlayer {
|
||||
private var presenter: SAPlayerPresenter!
|
||||
private var player: AudioEngine?
|
||||
|
||||
/**
|
||||
Access the engine of the player. Engine is nil if player has not been initialized with audio.
|
||||
|
||||
- Important: Changes to the engine are not safe guarded, thus unknown behaviour can arise from changing the engine. Just be wary and read [documentation of AVAudioEngine](https://developer.apple.com/documentation/avfoundation/avaudioengine) well when modifying,
|
||||
*/
|
||||
public var engine: AVAudioEngine? {
|
||||
get {
|
||||
return player?.engine
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Corresponding to the overall volume of the player. Volume's default value is 1.0 and the range of valid values is 0.0 to 1.0. Volume is nil if no audio has been initialized yet.
|
||||
*/
|
||||
public var volume: Float? {
|
||||
get {
|
||||
return player?.engine.mainMixerNode.volume
|
||||
}
|
||||
|
||||
set {
|
||||
guard let value = newValue else { return }
|
||||
guard value >= 0.0 && value <= 1.0 else { return }
|
||||
|
||||
player?.engine.mainMixerNode.volume = value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Corresponding to the skipping forward button on the media player on the lockscreen. Default is set to 30 seconds.
|
||||
*/
|
||||
@@ -254,6 +281,14 @@ extension SAPlayer {
|
||||
self.mediaInfo = mediaInfo
|
||||
presenter.handlePlayStreamedAudio(withRemoteUrl: url)
|
||||
}
|
||||
|
||||
/**
|
||||
Resets the player to the state before initializing audio and setting media info.
|
||||
*/
|
||||
public func clear() {
|
||||
player = nil
|
||||
presenter.handleClear()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -60,6 +60,18 @@ class SAPlayerPresenter {
|
||||
urlKeyMap[url.key] = url
|
||||
}
|
||||
|
||||
func handleClear() {
|
||||
needle = nil
|
||||
duration = nil
|
||||
key = nil
|
||||
mediaInfo = nil
|
||||
delegate?.clearLockScreenInfo()
|
||||
|
||||
AudioClockDirector.shared.detachFromChangesInDuration(withID: durationRef)
|
||||
AudioClockDirector.shared.detachFromChangesInNeedle(withID: needleRef)
|
||||
AudioClockDirector.shared.detachFromChangesInPlayingStatus(withID: playingStatusRef)
|
||||
}
|
||||
|
||||
func handlePlaySavedAudio(withSavedUrl url: URL) {
|
||||
attachForUpdates(url: url)
|
||||
delegate?.startAudioDownloaded(withSavedUrl: url)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// Data.swift
|
||||
// SwiftAudioPlayer
|
||||
//
|
||||
// Created by Tanha Kabir on 2019-11-29.
|
||||
// Copyright © 2019 Tanha Kabir, Jon Mercer
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Data {
|
||||
// Introduced in Swift 5, withUnsafeBytes using UnsafePointers is deprecated
|
||||
// https://mjtsai.com/blog/2019/03/27/swift-5-released/
|
||||
func accessBytes<R>(_ body: (UnsafePointer<UInt8>) throws -> R) rethrows -> R {
|
||||
return try withUnsafeBytes { (rawBufferPointer: UnsafeRawBufferPointer) -> R in
|
||||
let unsafeBufferPointer = rawBufferPointer.bindMemory(to: UInt8.self)
|
||||
guard let unsafePointer = unsafeBufferPointer.baseAddress else {
|
||||
Log.error("")
|
||||
var int: UInt8 = 0
|
||||
return try body(&int)
|
||||
}
|
||||
return try body(unsafePointer)
|
||||
}
|
||||
}
|
||||
|
||||
mutating func accessMutableBytes<R>(_ body: (UnsafeMutablePointer<UInt8>) throws -> R) rethrows -> R {
|
||||
return try withUnsafeMutableBytes { (rawBufferPointer: UnsafeMutableRawBufferPointer) -> R in
|
||||
let unsafeMutableBufferPointer = rawBufferPointer.bindMemory(to: UInt8.self)
|
||||
guard let unsafeMutablePointer = unsafeMutableBufferPointer.baseAddress else {
|
||||
Log.error("")
|
||||
var int: UInt8 = 0
|
||||
return try body(&int)
|
||||
}
|
||||
return try body(unsafeMutablePointer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'SwiftAudioPlayer'
|
||||
s.version = '2.1.0'
|
||||
s.version = '2.4.0'
|
||||
s.summary = 'SwiftAudioPlayer is a Swift based audio player that can handle streaming from a remote location and audio manipulation.'
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
|
||||
Reference in New Issue
Block a user