ios - AVPlayer "congela" la aplicación al inicio del almacenamiento en búfer de una transmisión de audio
audio-streaming dropbox (3)
Estoy usando una subclase de AVQueuePlayer
y cuando agrego un nuevo AVPlayerItem
con una URL de transmisión, la aplicación se congela durante uno o dos segundos. Al congelarme quiero decir que no responde a los toques en la interfaz de usuario. Además, si ya tengo una canción en reproducción y luego agrego otra a la cola, AVQueuePlayer
comienza a cargar automáticamente la canción mientras todavía está transmitiendo la primera. Esto hace que la aplicación no responda a los toques en la IU durante dos segundos, al igual que cuando se agrega la primera canción, pero la canción todavía se está reproduciendo. Entonces eso significa que AVQueuePlayer
está haciendo algo en el hilo principal que está causando el aparente "congelamiento".
Estoy usando insertItem:afterItem:
para agregar mi AVPlayerItem
. Probé y me aseguré de que este fuera el método que causaba el retraso. Quizás podría ser algo que AVPlayerItem
hace cuando AVPlayerItem
lo activa al momento de agregarlo a la cola.
Debo señalar que estoy usando Dropbox API v1 beta para obtener la URL de transmisión mediante esta llamada a método:
[[self restClient] loadStreamableURLForFile:metadata.path];
Luego, cuando reciba la URL de la AVQueuePlayer
envío a AVQueuePlayer
siguiente manera:
[self.player insertItem:[AVPlayerItem playerItemWithURL:url] afterItem:nil];
Entonces mi pregunta es: ¿cómo puedo evitar esto? ¿Debería realizar la precarga de una transmisión de audio por mi cuenta sin la ayuda de AVPlayer
? Si es así, ¿cómo hago esto?
Gracias.
Bump, ya que esta es una pregunta altamente calificada y preguntas similares en línea tienen respuestas obsoletas o no son geniales. La idea general es bastante directa con AVKit
y AVFoundation
, lo que significa que ya no dependerá de bibliotecas de terceros. El único problema es que requirió algunos ajustes y unió las piezas.
AVFoundation
de AVFoundation
Player()
con url
aparentemente no es segura para subprocesos, o mejor dicho, no está destinada a ser. Lo que significa que, sin importar cómo lo inicialices en un subproceso en segundo plano, los atributos del jugador se UITableView
en la cola principal, lo que provocará bloqueos en la UI, especialmente en UITableView
y UICollectionViews
. Para resolver este problema, Apple proporcionó AVAsset
que toma una URL y ayuda a cargar los atributos de los medios como pista, reproducción, duración, etc. y puede hacerlo de forma asíncrona, y la mejor parte es que este proceso de carga es cancelable (a diferencia de otros hilos de fondo de cola de despacho). donde terminar una tarea puede no ser tan sencillo). Esto significa que no hay necesidad de preocuparse por la persistencia de los subprocesos zombi en segundo plano a medida que se desplaza rápidamente en una vista de tabla o de colección, finalmente acumulando en la memoria un montón de objetos no utilizados. Esta característica cancelable es excelente y nos permite cancelar cualquier carga asíncrona AVAsset
persistente si está en progreso, pero solo durante la eliminación de la cola de la celda. El proceso de carga asincrónica puede invocarse mediante el método loadValuesAsynchronously
y puede cancelarse (según lo desee) en cualquier momento posterior (si todavía está en curso).
No se olvide de manejar de manera correcta los resultados de loadValuesAsynchronously
. En Swift (3/4) , así es como cargarías un video de forma asíncrona y manejarías situaciones si el proceso asíncrono falla (debido a redes lentas, etc.) -
TL; DR
PARA JUGAR UN VIDEO
let asset = AVAsset(url: URL(string: self.YOUR_URL_STRING))
let keys: [String] = ["playable"]
var player: AVPlayer!
asset.loadValuesAsynchronously(forKeys: keys, completionHandler: {
var error: NSError? = nil
let status = asset.statusOfValue(forKey: "playable", error: &error)
switch status {
case .loaded:
DispatchQueue.main.async {
let item = AVPlayerItem(asset: asset)
self.player = AVPlayer(playerItem: item)
let playerLayer = AVPlayerLayer(player: self.player)
playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
playerLayer.frame = self.YOUR_VIDEOS_UIVIEW.bounds
self.YOUR_VIDEOS_UIVIEW.layer.addSublayer(playerLayer)
self.player.isMuted = true
self.player.play()
}
break
case .failed:
DispatchQueue.main.async {
//do something, show alert, put a placeholder image etc.
}
break
case .cancelled:
DispatchQueue.main.async {
//do something, show alert, put a placeholder image etc.
}
break
default:
break
}
})
NOTA :
En función de lo que tu aplicación quiera lograr, es posible que aún tengas que hacer algunos ajustes para sintonizarla para obtener un desplazamiento más suave en una UITableView
o UICollectionView
. También es posible que necesite implementar una cierta cantidad de KVO en las propiedades de AVPlayerItem
para que funcione y hay muchas publicaciones aquí en SO que tratan AVPlayerItem
KVO en detalle.
PARA BUCLE A TRAVÉS DE ACTIVOS (video loops / GIF)
Para reproducir un video, puede usar el mismo método anterior e introducir AVPlayerLooper
. Aquí hay un código de muestra para reproducir un video (o tal vez un video corto en estilo GIF). Tenga en cuenta el uso de la clave de duration
que se requiere para nuestro video loop.
let asset = AVAsset(url: URL(string: self.YOUR_URL_STRING))
let keys: [String] = ["playable","duration"]
var player: AVPlayer!
var playerLooper: AVPlayerLooper!
asset.loadValuesAsynchronously(forKeys: keys, completionHandler: {
var error: NSError? = nil
let status = asset.statusOfValue(forKey: "duration", error: &error)
switch status {
case .loaded:
DispatchQueue.main.async {
let playerItem = AVPlayerItem(asset: asset)
self.player = AVQueuePlayer()
let playerLayer = AVPlayerLayer(player: self.player)
//define Timerange for the loop using asset.duration
let duration = playerItem.asset.duration
let start = CMTime(seconds: duration.seconds * 0, preferredTimescale: duration.timescale)
let end = CMTime(seconds: duration.seconds * 1, preferredTimescale: duration.timescale)
let timeRange = CMTimeRange(start: start, end: end)
self.playerLooper = AVPlayerLooper(player: self.player as! AVQueuePlayer, templateItem: playerItem, timeRange: timeRange)
playerLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
playerLayer.frame = self.YOUR_VIDEOS_UIVIEW.bounds
self.YOUR_VIDEOS_UIVIEW.layer.addSublayer(playerLayer)
self.player.isMuted = true
self.player.play()
}
break
case .failed:
DispatchQueue.main.async {
//do something, show alert, put a placeholder image etc.
}
break
case .cancelled:
DispatchQueue.main.async {
//do something, show alert, put a placeholder image etc.
}
break
default:
break
}
})
EDITAR : Según la documentation , AVPlayerLooper
requiere que la propiedad de duration
del activo esté completamente cargada para poder recorrer los videos. Además, el timeRange: timeRange
con el timeRange: timeRange
inicial y final en la inicialización de AVPlayerLooper
es realmente opcional si desea un bucle infinito. También me he dado cuenta desde que AVPlayerLooper
esta respuesta que AVPlayerLooper
tiene una AVPlayerLooper
solo un 70-80% en la AVAsset
bucles de videos, especialmente si su AVAsset
necesita transmitir el video desde una URL. Para resolver este problema, existe un enfoque totalmente diferente (aunque simple) para reproducir un video:
//this will loop the video since this is a Gif
let interval = CMTime(value: 1, timescale: 2)
self.timeObserverToken = self.player?.addPeriodicTimeObserver(forInterval: interval, queue: DispatchQueue.main, using: { (progressTime) in
if let totalDuration = self.player?.currentItem?.duration{
if progressTime == totalDuration{
self.player?.seek(to: kCMTimeZero)
self.player?.play()
}
}
})
La respuesta de Gigisommo para Swift 3 incluye los comentarios de los comentarios:
let asset = AVAsset(url: url)
let keys: [String] = ["playable"]
asset.loadValuesAsynchronously(forKeys: keys) {
DispatchQueue.main.async {
let item = AVPlayerItem(asset: asset)
self.playerCtrl.player = AVPlayer(playerItem: item)
}
}
No use playerItemWithURL
es sincronización. Cuando reciba la respuesta con la URL, intente esto:
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:url options:nil];
NSArray *keys = @[@"playable"];
[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^() {
[self.player insertItem:[AVPlayerItem playerItemWithAsset:asset] afterItem:nil];
}];