Создание пользовательского медиаплеера с поддержкой плейлиста в Ios

Предоставление бесшовного мультимедийного опыта в приложении iOS требует больше, чем просто падение в базовом . Современные пользователи ожидают сложных функций, включая динамическое управление плейлистом, фоновое воспроизведение, тесную интеграцию с пользовательским интерфейсом Now Playing на экране блокировки и Центре управления, а также безупречную обработку прерываний, таких как телефонные звонки. Переход за пределы простого просмотра игрока к полностью функциональному, готовому к производству медиа-движку требует тщательного архитектурного планирования, глубокого понимания структуры AVFoundation и строгого соблюдения лучших практик iOS-медиа.

В этой статье представлено всеобъемлющее руководство по созданию пользовательского медиаплеера с надежной поддержкой плейлиста. Мы рассмотрим основные архитектуры, стратегии реализации с использованием Swift и AVFoundation , соображения пользовательского интерфейса / UX и критические шаги, необходимые для глубокой интеграции вашего игрока в экосистему iOS.

Определение основных требований и архитектуры

Перед написанием одной строки кода важно установить четкий набор требований. Четко определенный объем предотвращает архитектурный дрейф и гарантирует, что ваш игрок соответствует ожиданиям пользователей как по функциональности, так и по производительности.

Функциональные требования

Архитектурные соображения

Для приложения, богатого медиа, шаблон Model-View-ViewModel (MVVM) , часто в сочетании с Combine или SwiftUI's ObservableObject, обеспечивает чистое разделение проблем. Ваши основные компоненты должны быть абстрагированы в отдельные слои:

Это разделение делает систему проверяемой, поддерживаемой и адаптируемой к будущим запросам функций, таким как добавление аудиоэквалайзеров или поддержка воспроизведения видео.

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 системного уровня. Сосредоточьтесь на деталях управления состоянием, обработки прерываний и обратной связи с пользователем. Следуя шаблонам, изложенным в этом руководстве, вы будете хорошо оснащены для создания медиаплеера, который отвечает высоким ожиданиям сегодняшних пользователей.