Создание пользовательского медиаплеера с поддержкой плейлиста в Ios
Предоставление бесшовного мультимедийного опыта в приложении iOS требует больше, чем просто падение в базовом . Современные пользователи ожидают сложных функций, включая динамическое управление плейлистом, фоновое воспроизведение, тесную интеграцию с пользовательским интерфейсом Now Playing на экране блокировки и Центре управления, а также безупречную обработку прерываний, таких как телефонные звонки. Переход за пределы простого просмотра игрока к полностью функциональному, готовому к производству медиа-движку требует тщательного архитектурного планирования, глубокого понимания структуры AVFoundation и строгого соблюдения лучших практик iOS-медиа.
В этой статье представлено всеобъемлющее руководство по созданию пользовательского медиаплеера с надежной поддержкой плейлиста. Мы рассмотрим основные архитектуры, стратегии реализации с использованием Swift и AVFoundation , соображения пользовательского интерфейса / UX и критические шаги, необходимые для глубокой интеграции вашего игрока в экосистему iOS.
Определение основных требований и архитектуры
Перед написанием одной строки кода важно установить четкий набор требований. Четко определенный объем предотвращает архитектурный дрейф и гарантирует, что ваш игрок соответствует ожиданиям пользователей как по функциональности, так и по производительности.
Функциональные требования
- Управление воспроизведением: Стандартные элементы управления, такие как Play, Pause, Seek Forward/Backward и настройка громкости.
- Управление плейлистом: Возможность создавать, сохранять, переупорядочивать, перетасовывать и повторять очередь медиа-элементов.
- Теперь игра Интеграция: Отображение текущих метаданных трека (титул, художник, произведение искусства) на экране блокировки и Центре управления.
- Справочное аудио: Продолжение воспроизведения при отправке приложения в фоновый режим.
- Удаленная обработка команд: Реагирование на аппаратные средства управления, устройства Bluetooth и CarPlay.
- Обработка прерываний: Грациозно приостанавливается и возобновляет воспроизведение во время телефонных звонков или тревог.
Архитектурные соображения
Для приложения, богатого медиа, шаблон Model-View-ViewModel (MVVM) , часто в сочетании с Combine или SwiftUI's ObservableObject, обеспечивает чистое разделение проблем. Ваши основные компоненты должны быть абстрагированы в отдельные слои:
- Мощность воспроизведения: Обертывание уровня обслуживания и управление состояниями воспроизведения.
- Менеджер плейлистов: Слой данных, обрабатывающий очередь, историю, настойчивость и логику перетасовки/повторения.
- Теперь Playing Service: Компонент, предназначенный для обновления и ответа на удаленные команды.
- Пользовательский уровень интерфейса: Виды SwiftUI или UIKit, которые наблюдают за состоянием движка воспроизведения и менеджера плейлиста.
Это разделение делает систему проверяемой, поддерживаемой и адаптируемой к будущим запросам функций, таким как добавление аудиоэквалайзеров или поддержка воспроизведения видео.
Deep Dive в AVFoundation и AVQueuePlayer
AVFoundation является основой обработки медиаданных на платформах Apple. подходит для воспроизведения одного элемента, , подкласс , специально разработан для управления очередями объектов. Он обрабатывает автоматическое продвижение к следующему треку и предоставляет методы манипулирования очередями.
Настройка игрока Queue
Инициирование просто. Вы предоставляете множество экземпляров . Каждый обертывает медиа-URL, который может быть локальным файловым путем или удаленным потоковым URL.
import AVFoundation
let urls: [URL] = [mediaURL1, mediaURL2, mediaURL3]
let playerItems = urls.map { AVPlayerItem(url: $0) }
let queuePlayer = AVQueuePlayer(items: playerItems)
queuePlayer.play()
Эта одиночная установка отвечает основным требованиям последовательного воспроизведения. Однако производственное приложение редко имеет статическую очередь. Нужно динамически добавлять, удалять и переупорядочивать элементы.
Динамическое управление очередями
предоставляет методы вставки и удаления элементов.Однако управление очередей исключительно с помощью этих методов может стать сложным, особенно при отслеживании в данный момент играющего предмета и его положения в очереди.
Лучшая практика — это поддержка отдельного массива резервного копирования в вашем Менеджере плейлистов. Это служит «источником истины». Когда пользователь переупорядочен трек, вы обновляете свой массив источников и восстанавливаете очередь . Хотя это звучит неэффективно, это самый надежный способ поддерживать синхронизацию между пользовательским интерфейсом и игроком.
func rebuildQueue() {
queuePlayer.removeAllItems()
for item in currentPlaylistItems {
queuePlayer.insert(item, after: nil)
}
// Ensure the player starts playing the correct item if playback was active.
guard let currentItem = currentPlaylistItems[safe: currentIndex] else { return }
queuePlayer.replaceCurrentItem(with: currentItem)
}
Обсуждение Playback State
Чтобы создать адаптивный пользовательский интерфейс, вы должны наблюдать изменения в состоянии игрока. Ключевые свойства для наблюдения включают:
- : Указывает, может ли игрок играть, неизвестен или столкнулся с ошибкой.
- : Указывает, играет ли игрок, приостанавливается или ждет улучшения условий сети.
- : Пожар, когда игрок переходит на следующий трек в очереди.
Используя Key-Value Observing (KVO) или, в идеале, Combine фреймворк, вы можете реактивно обновить свою ViewModel. Combine обеспечивает чистый, композитный способ наблюдения за этими свойствами.
import Combine
var cancellables = Set()
queuePlayer.publisher(for: \.timeControlStatus)
.sink { [weak self] status in
switch status {
case .playing: self?.isPlaying = true
case .paused: self?.isPlaying = false
case .waitingToPlayAtSpecifiedRate: self?.showLoadingIndicator = true
@unknown default: break
}
}
.store(in: &cancellables)
queuePlayer.publisher(for: \.currentItem)
.sink { [weak self] item in
// Update the UI to reflect the new track
self?.updateNowPlayingInfo(for: item)
self?.updateCurrentIndex()
}
.store(in: &cancellables)
Создайте надежного менеджера плейлистов
Менеджер плейлистов действует как слой данных для медиаплеера, он отделяет логику того, что играть, от логики того, как играть.
Модели данных
Создайте четкие, неизменяемые модели данных, чтобы представить ваши медиа.
struct MediaItem: Identifiable, Codable {
let id: UUID
let title: String
let artist: String
let albumArtURL: URL?
let mediaURL: URL
let duration: Double
}
struct Playlist: Identifiable, Codable {
let id: UUID
var name: String
var items: [MediaItem]
}
настойчивость
Пользователи ожидают, что их плейлисты будут сохраняться во время запуска приложений. Для простых данных плейлиста часто достаточно хранилища файлов или JSON в каталоге Документов. Для более сложных медиа-библиотек с тысячами элементов Основные данные или SwiftData обеспечивает эффективные механизмы запроса и сбоев.
Простой подход заключается в кодировании массива плейлистов JSON и записи его в файл.
func savePlaylists(_ playlists: [Playlist]) throws {
let data = try JSONEncoder().encode(playlists)
let url = getDocumentsDirectory().appendingPathComponent("playlists.json")
try data.write(to: url, options: [.atomic, .completeFileProtection])
}
Реализация перетасовки и повторения
Перетасовка плейлиста не должна изменять исходный список. Вместо этого, реализуйте алгоритм перетасовки Фишера-Йетса на копии индексов плейлиста.
- Повторить: Игрок останавливается в конце очереди.
- Повторите один: Те же петли трека бесконечно. С этим можно справиться, наблюдая и ища назад к началу.
- Повторить все: Вся очередь перезапускается с самого начала.Когда последний пункт заканчивается, вы восстанавливаете очередь с оригинальным или перетасованным заказом и начинаете воспроизведение.
Создание пользовательского интерфейса
Пользовательский интерфейс является точкой контакта для ваших пользователей.Эффективный пользовательский интерфейс медиаплеера интуитивно понятен, отзывчив и информативен.
SwiftUI соображения
Независимо от того, используете ли вы SwiftUI или UIKit, ViewModel должен выставлять чистый API для слоя просмотра.В SwiftUI ваш класс ViewModel может соответствовать .
class PlayerViewModel: ObservableObject {
@Published var currentItem: MediaItem?
@Published var isPlaying = false
@Published var currentTime: TimeInterval = 0
@Published var duration: TimeInterval = 0
func playPause() { ... }
func nextTrack() { ... }
func previousTrack() { ... }
func seek(to time: TimeInterval) { ... }
}
Затем ваши взгляды просматривают эту модель просмотра и автоматически обновляются.
struct PlayerView: View {
@StateObject var viewModel: PlayerViewModel
var body: some View {
VStack {
// Album Art, Title, Artist
Text(viewModel.currentItem?.title ?? "No Track")
.font(.title)
// Progress Slider
Slider(value: $viewModel.currentTime, in: 0...viewModel.duration) { editing in
if !editing {
viewModel.seek(to: viewModel.currentTime)
}
}
// Playback Controls
HStack {
Button(action: viewModel.previousTrack) { Image(systemName: "backward.fill") }
Button(action: viewModel.playPause) {
Image(systemName: viewModel.isPlaying ? "pause.fill" : "play.fill")
}
Button(action: viewModel.nextTrack) { Image(systemName: "forward.fill") }
}
}
}
}
Обновления и наблюдение за временем
Чтобы плавно обновить ползунок , необходимо периодически наблюдать время игрока. предоставляет . Будьте осторожны, чтобы обновить опубликованное свойство на главной очереди.
private var timeObserverToken: Any?
func setupTimeObserver() {
timeObserverToken = queuePlayer.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 1), queue: .main) { [weak self] time in
self?.viewModel.currentTime = time.seconds
}
}
Интеграция с системой контроля медиа iOS
Интеграция с Центром управления iOS и экраном блокировки не подлежит обсуждению для медиа-приложения. Ваше приложение должно взаимодействовать с системой через MPNowPlayingInfoCenter и MPRemoteCommandCenter.
Настройка аудио сессии
Прежде чем ваше приложение сможет воспроизводить аудио в фоновом режиме или отвечать на удаленные команды, вы должны настроить общий .
import AVFAudio
func setupAudioSession() {
let audioSession = AVAudioSession.sharedInstance()
do {
try audioSession.setCategory(.playback, mode: .default, options: [.allowAirPlay, .allowBluetooth])
try audioSession.setActive(true)
} catch {
print("Failed to set audio session category: \(error)")
}
}
Настройка категории на говорит iOS, что ваше приложение поддерживает воспроизведение фона. Вы также должны включить Аудио, AirPlay и Picture in Picture возможность в вкладке «Подпись и возможности» вашего проекта Xcode.
Population Now Playing Info (альбом)
Всякий раз, когда трек изменяется или происходит обновление состояния воспроизведения, вы должны обновить словарь в центре по умолчанию.
import MediaPlayer
func updateNowPlayingInfo(for item: AVPlayerItem) {
guard let mediaItem = viewModel.currentItem else {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
return
}
var nowPlayingInfo = [String: Any]()
nowPlayingInfo[MPMediaItemPropertyTitle] = mediaItem.title
nowPlayingInfo[MPMediaItemPropertyArtist] = mediaItem.artist
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = mediaItem.duration
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = queuePlayer.currentTime().seconds
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = queuePlayer.rate
// Asynchronously load artwork
if let artworkURL = mediaItem.albumArtURL {
URLSession.shared.dataTask(with: artworkURL) { data, _, _ in
if let data = data, let image = UIImage(data: data) {
let artwork = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
}.resume()
} else {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
}
}
Удаленная обработка команд
позволяет вашему приложению реагировать на команды с экрана блокировки, Центра управления и внешних аксессуаров.
func setupRemoteCommands() {
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.addTarget { [weak self] _ in
self?.queuePlayer.play()
return .success
}
commandCenter.pauseCommand.addTarget { [weak self] _ in
self?.queuePlayer.pause()
return .success
}
commandCenter.nextTrackCommand.addTarget { [weak self] _ in
self?.nextTrack()
return .success
}
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
guard let event = event as? MPChangePlaybackPositionCommandEvent else { return .commandFailed }
self?.queuePlayer.seek(to: CMTime(seconds: event.positionTime, preferredTimescale: 1))
return .success
}
}
Обработка крайних случаев и перерывов
Надежность является отличительной чертой профессионального приложения. Обработка системных прерываний и краевых кейсов изящно значительно улучшит пользовательский опыт.
Аудиоперерывы
Когда происходит телефонный звонок или сигнализация, iOS отключает аудиосессию. Вы должны зарегистрироваться для , чтобы приостановить воспроизведение, а затем возобновить его, как только прерывание закончится.
NotificationCenter.default.addObserver(forName: AVAudioSession.interruptionNotification, object: nil, queue: .main) { [weak self] notification in
guard let userInfo = notification.userInfo,
let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
let type = AVAudioSession.InterruptionType(rawValue: typeValue) else { return }
switch type {
case .began:
// Interruption started, pause playback
self?.queuePlayer.pause()
case .ended:
// Interruption ended, check if we should resume
guard let optionsValue = userInfo[AVAudioSessionInterruptionOptionKey] as? UInt else { return }
let options = AVAudioSession.InterruptionOptions(rawValue: optionsValue)
if options.contains(.shouldResume) {
self?.queuePlayer.play()
}
@unknown default:
break
}
}
Изменения маршрута
Пользователи часто подключают и отключают наушники. Ваше приложение должно соответствующим образом реагировать на эти изменения маршрута, как правило, путем приостановки воспроизведения, когда текущий маршрут исчезает.
NotificationCenter.default.addObserver(forName: AVAudioSession.routeChangeNotification, object: nil, queue: .main) { [weak self] notification in
guard let userInfo = notification.userInfo,
let reasonValue = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue) else { return }
if reason == .oldDeviceUnavailable {
// Headphones were unplugged
self?.queuePlayer.pause()
}
}
Сетевой буфер и ошибки
Потоковые носители склонны к прерываниям сети. Наблюдайте за игрой . Когда она становится , проверьте причину , чтобы решить, показывать ли буферный спиннер. Слушайте , чтобы изящно обрабатывать невосстановимые ошибки и автоматически пытаться перейти к следующему треку.
Расширенные возможности и оптимизация
Как только основной плеер работает безупречно, вы можете рассмотреть возможность добавления расширенных функций, чтобы порадовать своих пользователей.
Поддержка фото в картинке (PiP)
Для видеоконтента PiP является убийственной функцией. Для этого требуется , настроенная с . Убедитесь, что ваша аудиосессия настроена для PiP и что возможность Background Modes включает Audio, AirPlay и Picture in Picture.
AirPlay 2 Интеграция
и изначально поддерживают AirPlay. Вы можете контролировать его поведение, настроив . Используйте , чтобы обнаружить наличие доступных маршрутов AirPlay и отобразить кнопку громкости AirPlay в вашем пользовательском интерфейсе.
Управление производительностью и памятью
Медиа-игроки могут быть интенсивными.
- Артворное кэширование: Использование NSCache или выделенной библиотеки кэширования изображений (например, Kingfisher или SDWebImage) для предотвращения повторной загрузки альбома для каждого события воспроизведения.
- Предварительная загрузка: Для бесшовного опыта рассмотрите возможность предварительной загрузки следующего в очереди. Это можно сделать, создав заранее и позвонив на него.
- Очистка: Когда игрок находится в положении дел, не забудьте удалить наблюдателя времени и наблюдения KVO, чтобы предотвратить сохранение циклов и сбоев.
Лучшие практики для готовых к производству медиа-приложений
Поставка медиаплеера требует тщательного тестирования и соблюдения рекомендаций Apple.
Тестирование и обеспечение качества
- Единичное тестирование: Напишите тесты для перетасовки, повторения и переупорядочения логики вашего менеджера плейлистов. Они алгоритмически сложны и подвержены ошибкам.
- Сетевая симуляция: Используйте кондиционер сетевых ссылок для тестирования воспроизведения в плохих сетевых условиях. Убедитесь, что ваша логика буферизации и сообщения об ошибках удобны для пользователя.
- Проверка аппаратного обеспечения: Тестирование на нескольких типах устройств и версиях iOS. Обратите особое внимание на более старые устройства, где узкие места производительности более выражены.
Доступность
Сделайте медиаплеер доступным для всех. Используйте модификаторы SwiftUI и для описания назначения кнопок. Настройте параметры доступности слайдера соответствующим образом. Поддерживайте динамический тип, чтобы пользователи могли настраивать размер текста, не нарушая макет.
Руководящие принципы человеческого интерфейса
Руководящие принципы Apple по человеческому интерфейсу для воспроизведения медиа обеспечивают ценную основу для разработки интуитивно понятных интерфейсов. Следуйте стандартным конвенциям для управления транспортом, чтобы уменьшить кривую обучения пользователя.
Заключение
Создание пользовательского медиаплеера с поддержкой плейлиста на iOS является сложной, но полезной задачей. Он бросает вызов разработчикам сбалансировать адаптивный пользовательский интерфейс с надежным управлением звуком, системной интеграцией и устойчивостью к сети. Используя мощь AVQueuePlayer , отделяя логику через чистую архитектуру, такую как MVVM , и тщательно обрабатывая медиаэкосистему iOS через MPNowPlayingInfoCenter и AVAudioSession , вы можете создать медиаприложение профессионального уровня, которое выделяется в App Store.
Путешествие от простого цикла видео до полностью интегрированного медиа-движка включает в себя овладение этими API системного уровня. Сосредоточьтесь на деталях управления состоянием, обработки прерываний и обратной связи с пользователем. Следуя шаблонам, изложенным в этом руководстве, вы будете хорошо оснащены для создания медиаплеера, который отвечает высоким ожиданиям сегодняшних пользователей.