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