11 changed files with 387 additions and 105 deletions
@ -0,0 +1,24 @@ |
|||||
|
// swift-tools-version: 5.9 |
||||
|
import PackageDescription |
||||
|
|
||||
|
let package = Package( |
||||
|
name: "music_service", |
||||
|
platforms: [.iOS("18.0")], |
||||
|
products: [ |
||||
|
.library(name: "music-service", targets: ["music_service"]) |
||||
|
], |
||||
|
dependencies: [ |
||||
|
// 例如,添加 Alamofire 作为网络请求库 |
||||
|
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.6.0") |
||||
|
], |
||||
|
targets: [ |
||||
|
.target( |
||||
|
name: "music_service", |
||||
|
dependencies: [ |
||||
|
// 指定使用 Alamofire 库 |
||||
|
.product(name: "Alamofire", package: "Alamofire") |
||||
|
], |
||||
|
path: "Sources/music_service" |
||||
|
) |
||||
|
] |
||||
|
) |
||||
@ -0,0 +1,120 @@ |
|||||
|
// |
||||
|
// MusicService.swift |
||||
|
// music_service |
||||
|
// |
||||
|
// Created by 李伟 on 2025/6/4. |
||||
|
// |
||||
|
import Foundation |
||||
|
import AVFoundation |
||||
|
import MediaPlayer |
||||
|
import UIKit |
||||
|
|
||||
|
public class MusicService: NSObject { |
||||
|
public static let shared: MusicService = MusicService() |
||||
|
|
||||
|
private var player: AVPlayer? |
||||
|
private var playlist: [[String: String]] = [] |
||||
|
private var currentIndex = 0 |
||||
|
public var isPlaying = false |
||||
|
|
||||
|
private override init() { |
||||
|
super.init() |
||||
|
setupRemoteCommandCenter() |
||||
|
} |
||||
|
|
||||
|
func getCurrentUrlString() -> String? { |
||||
|
guard let urlAsset = player?.currentItem?.asset as? AVURLAsset else { |
||||
|
return nil |
||||
|
} |
||||
|
return urlAsset.url.absoluteString |
||||
|
} |
||||
|
|
||||
|
public func play(song: [String: String]) { |
||||
|
guard let urlString = song["url"], let url = URL(string: urlString) else { return } |
||||
|
player = AVPlayer(url: url) |
||||
|
player?.play() |
||||
|
isPlaying = true |
||||
|
updateNowPlayingInfo(song: song) |
||||
|
} |
||||
|
|
||||
|
public func playPlaylist(songs: [[String: String]]) { |
||||
|
playlist = songs |
||||
|
currentIndex = 0 |
||||
|
play(song: playlist[currentIndex]) |
||||
|
} |
||||
|
|
||||
|
func pause() { |
||||
|
player?.pause() |
||||
|
isPlaying = false |
||||
|
updatePlaybackState() |
||||
|
} |
||||
|
|
||||
|
func resume() { |
||||
|
player?.play() |
||||
|
isPlaying = true |
||||
|
updatePlaybackState() |
||||
|
} |
||||
|
|
||||
|
func next() { |
||||
|
guard currentIndex + 1 < playlist.count else { return } |
||||
|
currentIndex += 1 |
||||
|
play(song: playlist[currentIndex]) |
||||
|
} |
||||
|
|
||||
|
func previous() { |
||||
|
guard currentIndex - 1 >= 0 else { return } |
||||
|
currentIndex -= 1 |
||||
|
play(song: playlist[currentIndex]) |
||||
|
} |
||||
|
func stop() { |
||||
|
player?.pause() |
||||
|
player?.replaceCurrentItem(with: nil) |
||||
|
isPlaying = false |
||||
|
updatePlaybackState() |
||||
|
} |
||||
|
func setVolume(volume: Float) { |
||||
|
player?.volume = volume |
||||
|
} |
||||
|
private func setupRemoteCommandCenter() { |
||||
|
let commandCenter = MPRemoteCommandCenter.shared() |
||||
|
|
||||
|
commandCenter.playCommand.addTarget { [unowned self] _ in |
||||
|
self.resume() |
||||
|
return .success |
||||
|
} |
||||
|
|
||||
|
commandCenter.pauseCommand.addTarget { [unowned self] _ in |
||||
|
self.pause() |
||||
|
return .success |
||||
|
} |
||||
|
|
||||
|
commandCenter.nextTrackCommand.addTarget { [unowned self] _ in |
||||
|
self.next() |
||||
|
return .success |
||||
|
} |
||||
|
|
||||
|
commandCenter.previousTrackCommand.addTarget { [unowned self] _ in |
||||
|
self.previous() |
||||
|
return .success |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private func updateNowPlayingInfo(song: [String: String]) { |
||||
|
var nowPlayingInfo: [String: Any] = [ |
||||
|
MPMediaItemPropertyTitle: song["title"] ?? "Unknown Title", |
||||
|
MPMediaItemPropertyArtist: song["artist"] ?? "Unknown Artist" |
||||
|
] |
||||
|
|
||||
|
if let coverUrlString = song["coverUrl"], let coverUrl = URL(string: coverUrlString), |
||||
|
let data = try? Data(contentsOf: coverUrl), |
||||
|
let image = UIImage(data: data) { |
||||
|
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image } |
||||
|
} |
||||
|
|
||||
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo |
||||
|
} |
||||
|
|
||||
|
private func updatePlaybackState() { |
||||
|
// iOS manages playback state automatically, but you can update NowPlayingInfo if needed |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,71 @@ |
|||||
|
import Flutter |
||||
|
import UIKit |
||||
|
|
||||
|
public class MusicServicePlugin: NSObject, FlutterPlugin { |
||||
|
private static var channel: FlutterMethodChannel? |
||||
|
|
||||
|
public static func register(with registrar: FlutterPluginRegistrar) { |
||||
|
let channel = FlutterMethodChannel(name: "com.yunqiinnovation.music_service", binaryMessenger: registrar.messenger()) |
||||
|
let instance = MusicServicePlugin() |
||||
|
registrar.addMethodCallDelegate(instance, channel: channel) |
||||
|
self.channel = channel |
||||
|
} |
||||
|
|
||||
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { |
||||
|
print("Method call: \(call.method)") |
||||
|
|
||||
|
switch call.method { |
||||
|
case "play": |
||||
|
if let args = call.arguments as? [String: Any], |
||||
|
let song = args["song"] as? [String: String] { |
||||
|
MusicService.shared.play(song: song) |
||||
|
result(nil) |
||||
|
} else { |
||||
|
result(FlutterError(code: "INVALID_ARGUMENT", message: "Missing 'song' map", details: nil)) |
||||
|
} |
||||
|
|
||||
|
case "playlist": |
||||
|
if let args = call.arguments as? [String: Any], |
||||
|
let songs = args["songs"] as? [[String: String]], !songs.isEmpty { |
||||
|
// MusicServiceStarter.startServiceWithPlaylist(command: "playlist", songs: songs) |
||||
|
MusicService.shared.playPlaylist(songs: songs) |
||||
|
result(nil) |
||||
|
} else { |
||||
|
result(FlutterError(code: "INVALID_ARGUMENT", message: "Missing or empty 'songs' list", details: nil)) |
||||
|
} |
||||
|
|
||||
|
case "pause": |
||||
|
MusicService.shared.pause() |
||||
|
result(nil) |
||||
|
|
||||
|
case "resume": |
||||
|
MusicService.shared.resume() |
||||
|
result(nil) |
||||
|
|
||||
|
case "stop": |
||||
|
// MusicServiceStarter.startService(command: "stop") |
||||
|
MusicService.shared.stop() |
||||
|
result(nil) |
||||
|
|
||||
|
case "setVolume": |
||||
|
if let args = call.arguments as? [String: Any], |
||||
|
let volume = args["volume"] as? Double { |
||||
|
MusicService.shared.setVolume(volume: Float(volume)) |
||||
|
result(nil) |
||||
|
} else { |
||||
|
result(FlutterError(code: "INVALID_ARGUMENT", message: "Missing or invalid 'volume'", details: nil)) |
||||
|
} |
||||
|
|
||||
|
case "getStatus": |
||||
|
let isPlaying = MusicService.shared.isPlaying |
||||
|
let currentUrl = MusicService.shared.getCurrentUrlString() |
||||
|
let status: [String: Any] = [ |
||||
|
"isPlaying": isPlaying, |
||||
|
"url": currentUrl != nil ? currentUrl! : NSNull() |
||||
|
] |
||||
|
result(status) |
||||
|
default: |
||||
|
result(FlutterMethodNotImplemented) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue