Construcción de un Reproductor de Video Modular con SwiftUI y AVFoundation

Construcción de un Reproductor de Video Modular con SwiftUI y AVFoundation

La integración de la potencia declarativa de SwiftUI con las capacidades multimedia robustas de AVFoundation permite a los desarrolladores de iOS crear componentes de reproducción de video modernos y eficientes. A diferencia de los enfoques tradicionales basados en UIKit, donde la gestión del estado de la vista y la lógica de reproducción a menudo se entrelazan manualmente, el pardaigma impulsado por datos de SwiftUI facilita la construcción de reproductores más limpios y fáciles de mantener. Este artículo te guiará a través de los pasos esenciales para implementar un componente de control de video completo y reutilizable.

  1. Diseño del Controlador Central de Reproducción

El núcleo de cualquier reproductor de video reside en su gestión de estados. En SwiftUI, los tipos ObservableObject junto con los @Published property wrappers son ideales para crear una máquina de estados reactiva que refleje el estado actual del reproductor multimedia.

import Foundation
import AVFoundation
import Combine
import SwiftUI // Necesario para ViewModifier, etc.

class MediaPlaybackManager: ObservableObject {
    @Published var avPlayer: AVPlayer?
    @Published var playbackActive: Bool = false
    @Published var currentPlaybackTime: CMTime = .zero
    @Published var totalMediaDuration: CMTime = .zero
    @Published var userIsSeeking: Bool = false
    
    private var playerItemObserver: NSKeyValueObservation?
    private var timeMonitoringObserver: Any?
    
    init(videoURL: URL) {
        let playerItem = AVPlayerItem(url: videoURL)
        self.avPlayer = AVPlayer(playerItem: playerItem)
        setupPlayerObservers(item: playerItem)
        setupTimeMonitoring()
    }
    
    private func setupPlayerObservers(item: AVPlayerItem) {
        playerItemObserver = item.observe(\.duration, options: [.new]) { [weak self] observedItem, change in
            guard let newDuration = change.newValue, newDuration.isValid else { return }
            self?.totalMediaDuration = newDuration
        }
    }
    
    private func setupTimeMonitoring() {
        let updateInterval = CMTime(seconds: 0.75, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
        timeMonitoringObserver = avPlayer?.addPeriodicTimeObserver(
            forInterval: updateInterval,
            queue: .main) { [weak self] time in
                guard let self = self, !self.userIsSeeking else { return }
                self.currentPlaybackTime = time
            }
    }
    
    func togglePlaybackState() {
        if playbackActive {
            avPlayer?.pause()
        } else {
            avPlayer?.play()
        }
        playbackActive.toggle()
    }
    
    func seekToTime(targetTime: CMTime) {
        userIsSeeking = true
        avPlayer?.seek(to: targetTime, toleranceBefore: .zero, toleranceAfter: .zero) { [weak self] _ in
            self?.userIsSeeking = false
        }
    }
    
    deinit {
        if let observer = timeMonitoringObserver {
            avPlayer?.removeTimeObserver(observer)
        }
        playerItemObserver?.invalidate()
    }
}

Aspectos clave de este diseño:

  • Los atributos @Published garantizan que cualquier cambio en estas propiedades notifique a la interfaz de usuario para una actualización automática.
  • addPeriodicTimeObserver sincroniza el progreso de la reproducción con la interfaz de usuario a intervalos regulares.
  • Se utiliza un estado userIsSeeking independiente para evitar que la interfaz de usuario parpadee o se actualice erróneamente durante las operaciones de búsqueda.
  • Los observadores de tiempo y KVO se invalidan y eliminan en deinit para prevenir fugas de memoria.
  • La duración del elemento de reproducción se obtiene a través de KVO en el AVPlayerItem.
  1. Formateo de Tiempo de CMTime a String para la Interfaz

AVFoundation utiliza la estructura CMTime para representar el tiempo, pero la interfaz de usuario requiere una representación de cadena formateada. Podemos crear un ViewModifier reutilizable para este propósito.

import SwiftUI
import AVFoundation

struct TimeTextFormatterModifier: ViewModifier {
    let timeValue: CMTime
    
    private var formattedTimeString: String {
        guard !timeValue.isIndefinite && !timeValue.isNull && timeValue.seconds >= 0 else {
            return "--:--" // Muestra un formato predeterminado para tiempos no válidos
        }
        let totalSeconds = Int(timeValue.seconds)
        let minutes = totalSeconds / 60
        let seconds = totalSeconds % 60
        return String(format: "%02d:%02d", minutes, seconds)
    }
    
    func body(content: Content) -> some View {
        content
            .overlay(
                Text(formattedTimeString)
                    .font(.caption)
                    .foregroundColor(.white)
                    .padding(.horizontal, 6)
                    .padding(.vertical, 2)
                    .background(Color.black.opacity(0.7))
                    .cornerRadius(5)
            )
    }
}

// Extensión para aplicar el modificador fácilmente
extension View {
    func displayFormattedTime(time: CMTime) -> some View {
        self.modifier(TimeTextFormatterModifier(timeValue: time))
    }
}

Etiquetas: SwiftUI AVFoundation VideoPlayer iOSDevelopment MediaPlayback

Publicado el 8-23 04:24