{"id":16686375,"url":"https://github.com/yoheimuta/rxmusicplayer","last_synced_at":"2025-03-17T00:33:08.011Z","repository":{"id":39838363,"uuid":"208020816","full_name":"yoheimuta/RxMusicPlayer","owner":"yoheimuta","description":"A reactive library to make it easy for audio playbacks using RxSwift.","archived":false,"fork":false,"pushed_at":"2022-07-22T04:09:12.000Z","size":100466,"stargazers_count":58,"open_issues_count":18,"forks_count":8,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-10-19T19:36:09.313Z","etag":null,"topics":["audio","audio-player","avplayer","ios","music-player","rxswift","streaming-audio","swift","swiftui","swiftui-example"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yoheimuta.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-09-12T10:05:43.000Z","updated_at":"2024-07-17T05:14:12.000Z","dependencies_parsed_at":"2022-07-15T04:30:53.749Z","dependency_job_id":null,"html_url":"https://github.com/yoheimuta/RxMusicPlayer","commit_stats":null,"previous_names":[],"tags_count":16,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yoheimuta%2FRxMusicPlayer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yoheimuta%2FRxMusicPlayer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yoheimuta%2FRxMusicPlayer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yoheimuta%2FRxMusicPlayer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yoheimuta","download_url":"https://codeload.github.com/yoheimuta/RxMusicPlayer/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":221669444,"owners_count":16860879,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["audio","audio-player","avplayer","ios","music-player","rxswift","streaming-audio","swift","swiftui","swiftui-example"],"created_at":"2024-10-12T15:05:37.825Z","updated_at":"2024-10-27T11:36:24.681Z","avatar_url":"https://github.com/yoheimuta.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# RxMusicPlayer\n\n[![Build Status](https://app.bitrise.io/app/24b27b1bde763767/status.svg?token=i0LQTpCPw6Sm_mObo3YqTw\u0026branch=master)](https://app.bitrise.io/app/24b27b1bde763767)\n[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)\n\u003ca href=\"https://github.com/apple/swift-package-manager\" alt=\"RxMusicPlayer on Swift Package Manager\" title=\"RxMusicPlayer on Swift Package Manager\"\u003e\u003cimg src=\"https://img.shields.io/badge/Swift%20Package%20Manager-compatible-brightgreen.svg\" /\u003e\u003c/a\u003e\n[![Version](https://img.shields.io/cocoapods/v/RxMusicPlayer.svg?style=flat)](http://cocoapods.org/pods/RxMusicPlayer)\n[![License](https://img.shields.io/cocoapods/l/RxMusicPlayer.svg?style=flat)](http://cocoapods.org/pods/RxMusicPlayer)\n\nRxMusicPlayer is a wrapper of avplayer backed by RxSwift to make it easy for audio playbacks.\n\n## Features\n\n- Following [the Audio Guidelines for User-Controlled Playback and Recording Apps](https://developer.apple.com/library/archive/documentation/Audio/Conceptual/AudioSessionProgrammingGuide/AudioGuidelinesByAppType/AudioGuidelinesByAppType.html#//apple_ref/doc/uid/TP40007875-CH11-SW1).\n- Support for streaming both remote and local audio files.\n- Functions to `play`, `pause`, `stop`, `play next`, `play previous`, `skip forward/backward`, `prefetch metadata`, `repeat mode(repeat, repeat all)`, `shuffle mode` `desired playback rate`, `seek to a certain second`, and `append/insert/remove an item into the playlist`.\n- Loading metadata, including `title`, `album`, `artist`, `artwork`, `duration`, and `lyrics`.\n- Background mode integration with MPNowPlayingInfoCenter.\n- Remote command control integration with MPRemoteCommandCenter.\n- Interruption handling with AVAudioSession.interruptionNotification.\n- Route change handling with AVAudioSession.routeChangeNotification.\n- Including a fully working example project, one built on UIKit and the other built on SwiftUI.\n\n## Runtime Requirements\n\n- iOS 10.0 or later\n\n## Installation\n\n### Swift Package Manager\n\nWith 2.0.1 and above.\n\n### Carthage\n\n```\ngithub \"yoheimuta/RxMusicPlayer\"\n```\n\n### CocoaPods\n\n```\npod \"RxMusicPlayer\"\n```\n\n## Usage\n\nFor details, refer to the [ExampleSwiftUI project](https://github.com/yoheimuta/RxMusicPlayer/tree/master/ExampleSwiftUI) or [Example project](https://github.com/yoheimuta/RxMusicPlayer/tree/master/Example).\nPlus, see also **Users** section below.\n\n## Example\n\n\u003cimg src=\"doc/example.gif\" alt=\"example\" width=\"300\"/\u003e\n\nYou can implement your audio player with the custom frontend without any delegates, like below.\n\n### Based on SwiftUI\n\n```swift\nimport SwiftUI\nimport Combine\nimport RxMusicPlayer\nimport RxSwift\nimport RxCocoa\n\nfinal class PlayerModel: ObservableObject {\n    private let disposeBag = DisposeBag()\n    private let player: RxMusicPlayer\n    private let commandRelay = PublishRelay\u003cRxMusicPlayer.Command\u003e()\n\n    @Published var canPlay = true\n    @Published var canPlayNext = true\n    @Published var canPlayPrevious = true\n    @Published var canSkipForward = true\n    @Published var canSkipBackward = true\n    @Published var title = \"Not Playing\"\n    @Published var artwork: UIImage?\n    @Published var restDuration = \"--:--\"\n    @Published var duration = \"--:--\"\n    @Published var shuffleMode = RxMusicPlayer.ShuffleMode.off\n    @Published var repeatMode = RxMusicPlayer.RepeatMode.none\n    @Published var remoteControl = RxMusicPlayer.RemoteControl.moveTrack\n\n    @Published var sliderValue = Float(0)\n    @Published var sliderMaximumValue = Float(0)\n    @Published var sliderIsUserInteractionEnabled = false\n    @Published var sliderPlayableProgress = Float(0)\n\n    private var cancelBag = Set\u003cAnyCancellable\u003e()\n    var sliderValueChanged = PassthroughSubject\u003cFloat, Never\u003e()\n\n    init() {\n        // 1) Create a player\n        let items = [\n            URL(string: \"https://storage.googleapis.com/great-dev/oss/musicplayer/tagmp3_1473200_1.mp3\")!,\n            URL(string: \"https://storage.googleapis.com/great-dev/oss/musicplayer/tagmp3_2160166.mp3\")!,\n            URL(string: \"https://storage.googleapis.com/great-dev/oss/musicplayer/tagmp3_4690995.mp3\")!,\n            Bundle.main.url(forResource: \"tagmp3_9179181\", withExtension: \"mp3\")!\n        ]\n        .map({ RxMusicPlayerItem(url: $0) })\n        player = RxMusicPlayer(items: items)!\n\n        // 2) Control views\n        player.rx.canSendCommand(cmd: .play)\n            .do(onNext: { [weak self] canPlay in\n                self?.canPlay = canPlay\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        player.rx.canSendCommand(cmd: .next)\n            .do(onNext: { [weak self] canPlayNext in\n                self?.canPlayNext = canPlayNext\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        player.rx.canSendCommand(cmd: .previous)\n            .do(onNext: { [weak self] canPlayPrevious in\n                self?.canPlayPrevious = canPlayPrevious\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        player.rx.canSendCommand(cmd: .seek(seconds: 0, shouldPlay: false))\n            .do(onNext: { [weak self] canSeek in\n                self?.sliderIsUserInteractionEnabled = canSeek\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        player.rx.canSendCommand(cmd: .skip(seconds: 15))\n            .do(onNext: { [weak self] canSkip in\n                self?.canSkipForward = canSkip\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        player.rx.canSendCommand(cmd: .skip(seconds: -15))\n            .do(onNext: { [weak self] canSkip in\n                self?.canSkipBackward = canSkip\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        player.rx.currentItemDuration()\n            .do(onNext: { [weak self] in\n                self?.sliderMaximumValue = Float($0?.seconds ?? 0)\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        player.rx.currentItemTime()\n            .do(onNext: { [weak self] time in\n                self?.sliderValue = Float(time?.seconds ?? 0)\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        player.rx.currentItemLoadedProgressRate()\n            .do(onNext: { [weak self] rate in\n                self?.sliderPlayableProgress = rate ?? 0\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        player.rx.currentItemTitle()\n            .do(onNext: { [weak self] title in\n                self?.title = title ?? \"\"\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        player.rx.currentItemArtwork()\n            .do(onNext: { [weak self] artwork in\n                self?.artwork = artwork\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        player.rx.currentItemRestDurationDisplay()\n            .do(onNext: { [weak self] duration in\n                self?.restDuration = duration ?? \"--:--\"\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        player.rx.currentItemTimeDisplay()\n            .do(onNext: { [weak self] duration in\n                if duration == \"00:00\" {\n                    self?.duration = \"0:00\"\n                    return\n                }\n                self?.duration = duration ?? \"--:--\"\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        player.rx.shuffleMode()\n            .do(onNext: { [weak self] mode in\n                self?.shuffleMode = mode\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        player.rx.repeatMode()\n            .do(onNext: { [weak self] mode in\n                self?.repeatMode = mode\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        player.rx.remoteControl()\n            .do(onNext: { [weak self] control in\n                self?.remoteControl = control\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        // 3) Process the user's input\n        player.run(cmd: commandRelay.asDriver(onErrorDriveWith: .empty()))\n            .flatMap { status -\u003e Driver\u003c()\u003e in\n                switch status {\n                case let RxMusicPlayer.Status.failed(err: err):\n                    print(err)\n                case let RxMusicPlayer.Status.critical(err: err):\n                    print(err)\n                default:\n                    print(status)\n                }\n                return .just(())\n            }\n            .drive()\n            .disposed(by: disposeBag)\n\n        commandRelay.accept(.prefetch)\n\n        sliderValueChanged\n            .removeDuplicates()\n            .sink { [weak self] value in\n                self?.seek(value: value)\n            }\n            .store(in: \u0026cancelBag)\n    }\n\n    func seek(value: Float?) {\n        commandRelay.accept(.seek(seconds: Int(value ?? 0), shouldPlay: false))\n    }\n\n    func skip(second: Int) {\n        commandRelay.accept(.skip(seconds: second))\n    }\n\n    func shuffle() {\n        switch player.shuffleMode {\n        case .off: player.shuffleMode = .songs\n        case .songs: player.shuffleMode = .off\n        }\n    }\n\n    func play() {\n        commandRelay.accept(.play)\n    }\n\n    func pause() {\n        commandRelay.accept(.pause)\n    }\n\n    func playNext() {\n        commandRelay.accept(.next)\n    }\n\n    func playPrevious() {\n        commandRelay.accept(.previous)\n    }\n\n    func doRepeat() {\n        switch player.repeatMode {\n        case .none: player.repeatMode = .one\n        case .one: player.repeatMode = .all\n        case .all: player.repeatMode = .none\n        }\n    }\n\n    func toggleRemoteControl() {\n        switch remoteControl {\n        case .moveTrack:\n            player.remoteControl = .skip(second: 15)\n        case .skip:\n            player.remoteControl = .moveTrack\n        }\n    }\n}\n\nstruct PlayerView: View {\n    @StateObject private var model = PlayerModel()\n\n    var body: some View {\n        ScrollView {\n            VStack {\n                Spacer()\n                    .frame(width: 1, height: 49)\n\n                if let artwork = model.artwork {\n                    Image(uiImage: artwork)\n                        .resizable()\n                        .scaledToFit()\n                        .frame(height: 276)\n                } else {\n                    Spacer()\n                        .frame(width: 1, height: 276)\n                }\n\n                ProgressSliderView(value: $model.sliderValue,\n                                   maximumValue: $model.sliderMaximumValue,\n                                   isUserInteractionEnabled: $model.sliderIsUserInteractionEnabled,\n                                   playableProgress: $model.sliderPlayableProgress) {\n                    model.sliderValueChanged.send($0)\n                }\n                    .padding(.horizontal)\n\n                HStack {\n                    Text(model.duration)\n                    Spacer()\n                    Text(model.restDuration)\n                }\n                .padding(.horizontal)\n\n                Spacer()\n                    .frame(width: 1, height: 17)\n\n                Text(model.title)\n\n                Spacer()\n                    .frame(width: 1, height: 19)\n\n                HStack(spacing: 20.0) {\n                    Button(action: {\n                        model.shuffle()\n                    }) {\n                        Text(model.shuffleMode == .off ? \"Shuffle\" : \"No Shuffle\")\n                    }\n\n                    Button(action: {\n                        model.playPrevious()\n                    }) {\n                        Text(\"Previous\")\n                    }\n                    .disabled(!model.canPlayPrevious)\n\n                    Button(action: {\n                        model.canPlay ? model.play() : model.pause()\n                    }) {\n                        Text(model.canPlay ? \"Play\" : \"Pause\")\n                    }\n\n                    Button(action: {\n                        model.playNext()\n                    }) {\n                        Text(\"Next\")\n                    }\n                    .disabled(!model.canPlayNext)\n\n                    Button(action: {\n                        model.doRepeat()\n                    }) {\n                        Text({\n                            switch model.repeatMode {\n                            case .none: return \"Repeat\"\n                            case .one: return \"Repeat(All)\"\n                            case .all: return \"No Repeat\"\n                            }\n                        }() as String)\n                    }\n                }\n\n                Group {\n                    Spacer()\n                        .frame(width: 1, height: 17)\n\n                    HStack(spacing: 20.0) {\n                        Button(action: {\n                            model.skip(second: -15)\n                        }) {\n                            Text(\"SkipBackward\")\n                        }\n                        .disabled(!model.canSkipBackward)\n\n                        Button(action: {\n                            model.skip(second: 15)\n                        }) {\n                            Text(\"SkipForward\")\n                        }\n                        .disabled(!model.canSkipForward)\n                    }\n                }\n\n                Group {\n                    Spacer()\n                        .frame(width: 1, height: 17)\n\n                    HStack(spacing: 20.0) {\n                        Button(action: {\n                            model.toggleRemoteControl()\n                        }) {\n                            let control = model.remoteControl == .moveTrack ? \"moveTrack\" : \"skip\"\n                            Text(\"RemoteControl: \\(control)\")\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\nstruct PlayerView_Previews: PreviewProvider {\n    static var previews: some View {\n        PlayerView()\n    }\n}\n```\n\n### Based on UIKit\n\n```swift\nimport RxCocoa\nimport RxMusicPlayer\nimport RxSwift\nimport UIKit\n\nclass TableViewController: UITableViewController {\n\n    @IBOutlet private var playButton: UIButton!\n    @IBOutlet private var nextButton: UIButton!\n    @IBOutlet private var prevButton: UIButton!\n    @IBOutlet private var titleLabel: UILabel!\n    @IBOutlet private var artImageView: UIImageView!\n    @IBOutlet private var lyricsLabel: UILabel!\n    @IBOutlet private var seekBar: ProgressSlider!\n    @IBOutlet private var seekDurationLabel: UILabel!\n    @IBOutlet private var durationLabel: UILabel!\n    @IBOutlet private var shuffleButton: UIButton!\n    @IBOutlet private var repeatButton: UIButton!\n    @IBOutlet private var rateButton: UIButton!\n    @IBOutlet private var appendButton: UIButton!\n    @IBOutlet private var changeButton: UIButton!\n\n    private let disposeBag = DisposeBag()\n\n    // swiftlint:disable cyclomatic_complexity\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        // 1) Create a player\n        let items = [\n            \"https://storage.googleapis.com/great-dev/oss/musicplayer/tagmp3_1473200_1.mp3\",\n            \"https://storage.googleapis.com/great-dev/oss/musicplayer/tagmp3_2160166.mp3\",\n            \"https://storage.googleapis.com/great-dev/oss/musicplayer/tagmp3_4690995.mp3\",\n            \"https://storage.googleapis.com/great-dev/oss/musicplayer/tagmp3_9179181.mp3\",\n            \"https://storage.googleapis.com/great-dev/oss/musicplayer/bensound-extremeaction.mp3\",\n            \"https://storage.googleapis.com/great-dev/oss/musicplayer/bensound-littleplanet.mp3\",\n        ]\n        .map({ RxMusicPlayerItem(url: URL(string: $0)!) })\n        let player = RxMusicPlayer(items: Array(items[0 ..\u003c 4]))!\n\n        // 2) Control views\n        player.rx.canSendCommand(cmd: .play)\n            .do(onNext: { [weak self] canPlay in\n                self?.playButton.setTitle(canPlay ? \"Play\" : \"Pause\", for: .normal)\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        player.rx.canSendCommand(cmd: .next)\n            .drive(nextButton.rx.isEnabled)\n            .disposed(by: disposeBag)\n\n        player.rx.canSendCommand(cmd: .previous)\n            .drive(prevButton.rx.isEnabled)\n            .disposed(by: disposeBag)\n\n        player.rx.canSendCommand(cmd: .seek(seconds: 0, shouldPlay: false))\n            .drive(seekBar.rx.isUserInteractionEnabled)\n            .disposed(by: disposeBag)\n\n        player.rx.currentItemTitle()\n            .drive(titleLabel.rx.text)\n            .disposed(by: disposeBag)\n\n        player.rx.currentItemArtwork()\n            .drive(artImageView.rx.image)\n            .disposed(by: disposeBag)\n\n        player.rx.currentItemLyrics()\n            .distinctUntilChanged()\n            .do(onNext: { [weak self] _ in\n                self?.tableView.reloadData()\n            })\n            .drive(lyricsLabel.rx.text)\n            .disposed(by: disposeBag)\n\n        player.rx.currentItemRestDurationDisplay()\n            .map {\n                guard let rest = $0 else { return \"--:--\" }\n                return \"-\\(rest)\"\n            }\n            .drive(durationLabel.rx.text)\n            .disposed(by: disposeBag)\n\n        player.rx.currentItemTimeDisplay()\n            .drive(seekDurationLabel.rx.text)\n            .disposed(by: disposeBag)\n\n        player.rx.currentItemDuration()\n            .map { Float($0?.seconds ?? 0) }\n            .do(onNext: { [weak self] in\n                self?.seekBar.maximumValue = $0\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        let seekValuePass = BehaviorRelay\u003cBool\u003e(value: true)\n        player.rx.currentItemTime()\n            .withLatestFrom(seekValuePass.asDriver()) { ($0, $1) }\n            .filter { $0.1 }\n            .map { Float($0.0?.seconds ?? 0) }\n            .drive(seekBar.rx.value)\n            .disposed(by: disposeBag)\n        seekBar.rx.controlEvent(.touchDown)\n            .do(onNext: {\n                seekValuePass.accept(false)\n            })\n            .subscribe()\n            .disposed(by: disposeBag)\n        seekBar.rx.controlEvent(.touchUpInside)\n            .do(onNext: {\n                seekValuePass.accept(true)\n            })\n            .subscribe()\n            .disposed(by: disposeBag)\n\n        player.rx.currentItemLoadedProgressRate()\n            .drive(seekBar.rx.playableProgress)\n            .disposed(by: disposeBag)\n\n        player.rx.shuffleMode()\n            .do(onNext: { [weak self] mode in\n                self?.shuffleButton.setTitle(mode == .off ? \"Shuffle\" : \"No Shuffle\", for: .normal)\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        player.rx.repeatMode()\n            .do(onNext: { [weak self] mode in\n                var title = \"\"\n                switch mode {\n                case .none: title = \"Repeat\"\n                case .one: title = \"Repeat(All)\"\n                case .all: title = \"No Repeat\"\n                }\n                self?.repeatButton.setTitle(title, for: .normal)\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        player.rx.playerIndex()\n            .do(onNext: { index in\n                if index == player.queuedItems.count - 1 {\n                    // You can remove the comment-out below to confirm the append().\n                    // player.append(items: items)\n                }\n            })\n            .drive()\n            .disposed(by: disposeBag)\n\n        // 3) Process the user's input\n        let cmd = Driver.merge(\n            playButton.rx.tap.asDriver().map { [weak self] in\n                if self?.playButton.currentTitle == \"Play\" {\n                    return RxMusicPlayer.Command.play\n                }\n                return RxMusicPlayer.Command.pause\n            },\n            nextButton.rx.tap.asDriver().map { RxMusicPlayer.Command.next },\n            prevButton.rx.tap.asDriver().map { RxMusicPlayer.Command.previous },\n            seekBar.rx.controlEvent(.valueChanged).asDriver()\n                .map { [weak self] _ in\n                    RxMusicPlayer.Command.seek(seconds: Int(self?.seekBar.value ?? 0),\n                                               shouldPlay: false)\n                }\n                .distinctUntilChanged()\n        )\n        .startWith(.prefetch)\n        .debug()\n\n        // You can remove the comment-out below to confirm changing the current index of music items.\n        // Default is 0.\n        // player.playIndex = 1\n\n        player.run(cmd: cmd)\n            .do(onNext: { status in\n                UIApplication.shared.isNetworkActivityIndicatorVisible = status == .loading\n            })\n            .flatMap { [weak self] status -\u003e Driver\u003c()\u003e in\n                guard let weakSelf = self else { return .just(()) }\n\n                switch status {\n                case let RxMusicPlayer.Status.failed(err: err):\n                    print(err)\n                    return Wireframe.promptOKAlertFor(src: weakSelf,\n                                                      title: \"Error\",\n                                                      message: err.localizedDescription)\n\n                case let RxMusicPlayer.Status.critical(err: err):\n                    print(err)\n                    return Wireframe.promptOKAlertFor(src: weakSelf,\n                                                      title: \"Critical Error\",\n                                                      message: err.localizedDescription)\n                default:\n                    print(status)\n                }\n                return .just(())\n            }\n            .drive()\n            .disposed(by: disposeBag)\n\n        shuffleButton.rx.tap.asDriver()\n            .drive(onNext: {\n                switch player.shuffleMode {\n                case .off: player.shuffleMode = .songs\n                case .songs: player.shuffleMode = .off\n                }\n            })\n            .disposed(by: disposeBag)\n\n        repeatButton.rx.tap.asDriver()\n            .drive(onNext: {\n                switch player.repeatMode {\n                case .none: player.repeatMode = .one\n                case .one: player.repeatMode = .all\n                case .all: player.repeatMode = .none\n                }\n            })\n            .disposed(by: disposeBag)\n\n        rateButton.rx.tap.asDriver()\n            .flatMapLatest { [weak self] _ -\u003e Driver\u003c()\u003e in\n                guard let weakSelf = self else { return .just(()) }\n\n                return Wireframe.promptSimpleActionSheetFor(\n                    src: weakSelf,\n                    cancelAction: \"Close\",\n                    actions: PlaybackRateAction.allCases.map {\n                        ($0.rawValue, player.desiredPlaybackRate == $0.toFloat)\n                })\n                    .do(onNext: { [weak self] action in\n                        if let rate = PlaybackRateAction(rawValue: action)?.toFloat {\n                            player.desiredPlaybackRate = rate\n                            self?.rateButton.setTitle(action, for: .normal)\n                        }\n                    })\n                    .map { _ in }\n            }\n            .drive()\n            .disposed(by: disposeBag)\n\n        appendButton.rx.tap.asDriver()\n            .do(onNext: {\n                let newItems = Array(items[4 ..\u003c 6])\n                player.append(items: newItems)\n            })\n            .drive(onNext: { [weak self] _ in\n                self?.appendButton.isEnabled = false\n            })\n            .disposed(by: disposeBag)\n\n        changeButton.rx.tap.asObservable()\n            .flatMapLatest { [weak self] _ -\u003e Driver\u003c()\u003e in\n                guard let weakSelf = self else { return .just(()) }\n\n                return Wireframe.promptSimpleActionSheetFor(\n                    src: weakSelf,\n                    cancelAction: \"Close\",\n                    actions: items.map {\n                        ($0.url.lastPathComponent, player.queuedItems.contains($0))\n                })\n                    .asObservable()\n                    .do(onNext: { action in\n                        if let idx = player.queuedItems.map({ $0.url.lastPathComponent }).firstIndex(of: action) {\n                            try player.remove(at: idx)\n                        } else if let idx = items.map({ $0.url.lastPathComponent }).firstIndex(of: action) {\n                            for i in (0 ... idx).reversed() {\n                                if let prev = player.queuedItems.firstIndex(of: items[i]) {\n                                    player.insert(items[idx], at: prev + 1)\n                                    break\n                                }\n                                if i == 0 {\n                                    player.insert(items[idx], at: 0)\n                                }\n                            }\n                        }\n\n                        self?.appendButton.isEnabled = !(player.queuedItems.contains(items[4])\n                            || player.queuedItems.contains(items[5]))\n                    })\n                    .asDriver(onErrorJustReturn: \"\")\n                    .map { _ in }\n            }\n            .asDriver(onErrorJustReturn: ())\n            .drive()\n            .disposed(by: disposeBag)\n    }\n}\n```\n\n## Users\n\n- AMMusicPlayerController\n  - https://github.com/yoheimuta/AMMusicPlayerController\n\n## Contributing\n\n- Fork it\n- Run `make bootstrap`\n- Create your feature branch: git checkout -b your-new-feature\n- Commit changes: git commit -m 'Add your feature'\n- Push to the branch: git push origin your-new-feature\n- Submit a pull request\n\n### Release\n\n- Create a new release on GitHub\n- Publish a new podspec on Cocoapods\n  - `bundle exec pod trunk push RxMusicPlayer.podspec`\n\n## Bug Report\n\nWhile any bug reports are helpful, it's sometimes unable to pinpoint the cause without a reproducible project.\n\nIn particular, since RxMusicPlayer depends on RxSwift that is prone to your application program mistakes, it's more essential to decouple the problem.\n\nTherefore, I highly recommend that you submit an issue with that project.\n\nYou can create it like the following steps.\n\n- Fork it\n- Create your feature branch: git checkout -b your-bug-name\n- Add some changes under the Example directory to reproduce the bug\n- Commit changes: git commit -m 'Add a reproducible feature'\n- Push to the branch: git push origin your-bug-name\n- (Optional) Submit a pull request\n- Share it in your issue\n\nThe code should not be intertwined but concise, straightforward, and naive.\n\nNOTE: If you can't prepare any reproducible code, you have to elaborate the detail precisely and clearly so that I can reproduce the problem.\n\n## License\n\nThe MIT License (MIT)\n\n## Acknowledgement\n\nThank you to the following projects and creators.\n\n- Jukebox: https://github.com/teodorpatras/Jukebox\n  - I inspired by this library for the interface and some implementation.\n- RxAudioVisual: https://github.com/keitaoouchi/RxAudioVisual\n  - I referred to some implementation instead of depending on it due to adaptation to the latest Swift compiler.\n- Smith, J.O. Physical Audio Signal Processing: https://ccrma.stanford.edu/~jos/waveguide/Sound_Examples.html\n  - This project is using these as sample mp3 files.\n- file-examples.com: https://file-examples.com/index.php/sample-audio-files/sample-mp3-download/\n  - This project is using one as a sample mp3 file.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyoheimuta%2Frxmusicplayer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyoheimuta%2Frxmusicplayer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyoheimuta%2Frxmusicplayer/lists"}