Estamos muy ocupados trabajando para que la reproducción HLS en el reproductor de Brightcove sea mejor, más rápida y más estable. Para ello, hemos tenido que desechar nuestras suposiciones y empezar a analizar el problema al que nos enfrentábamos sin ideas preconcebidas.
EL RETO
Una de las principales responsabilidades de cualquier motor de reproducción que aproveche las Media Source Extensions (MSE) es tomar decisiones sobre qué datos de vídeo (denominados segmentos o fragmentos) solicitar al servidor en cada momento.
Con las fuentes HLS de vídeo a la carta (VOD), las decisiones son bastante sencillas. Conocemos todos los segmentos y (aproximadamente) su duración. A partir de esta información, es fácil decidir qué segmento descargar.
Por desgracia, las cosas no son tan sencillas en una transmisión HLS en directo. No sólo carecemos de todo el historial de segmentos, sino que sin el PROGRAM-DATETIME
en HLS (una adición reciente a la especificación HLS), tampoco disponemos de una forma sencilla de correlacionar segmentos a través de listas de reproducción variantes. La única opción que le queda al reproductor es descargar especulativamente segmentos para utilizar las marcas de tiempo internas de los medios.
En resumen, el problema de la reproducción en directo es que hay veces en que las muchas "incógnitas" hacen que seleccionar el segmento correcto la primera vez sea una tarea difícil.
ALGORITMO FETCH
Para combatir la tendencia de cualquier algoritmo de búsqueda a seleccionar el segmento equivocado, hemos tomado prestados algunos conceptos de la teoría del control. Antes, el algoritmo de búsqueda..:
- Hacer la mejor estimación posible teniendo en cuenta la información limitada
- Si la suposición fue errónea, utilice la información obtenida de esa solicitud para hacer una suposición mejor (reduzca el "error").
- Repita
La esperanza era que el algoritmo mejorara iterativamente y acabara descargando el segmento correcto. El problema surge cuando se empieza a considerar qué es un "error". Para nuestro algoritmo, definimos error como una región del búfer de vídeo a la que le faltaban datos.
La idea es que si buscamos el segmento A seguido del segmento C, habríamos creado un hueco del tamaño de B que habría que rellenar. El algoritmo debería entonces retroceder para rellenar ese error y seleccionar el segmento B antes de continuar hacia D.
La buena noticia es que el 99% de las veces funcionaba y lo hacía bastante bien. La mala noticia es que el 1% de las veces se quedaba atascado intentando rellenar un hueco que esencialmente no se podía rellenar. Cuando esto ocurría, normalmente se debía a la naturaleza de las fuentes que estábamos reproduciendo. Algunas fuentes HLS están mal segmentadas, de modo que el audio y el vídeo no se segmentan en el mismo momento en todas las variantes, lo que provoca huecos. Algunas fuentes HLS tienen fotogramas corruptos o faltantes (audio o vídeo) que también provocan lagunas.
Fuera cual fuera la causa, estas partes no rellenables del búfer creaban situaciones en las que el algoritmo se quedaba atascado intentando llenarlo. Al final, incorporamos varios métodos para evitar que el fetcher se atascara:
- Considerar los huecos muy pequeños como algo intrínseco a la fuente e ignorarlos
- Forzar al algoritmo a buscar uno o más segmentos hacia adelante si alguna vez intentó buscar el mismo segmento que durante la última iteración.
- Considerar cargados los segmentos cuyos límites estuvieran representados en el búfer en un 90% o más para evitar el derroche innecesario de ancho de banda.
El problema con cada una de estas estrategias es que tienen circunstancias muy específicas en las que se estropean. Con cada "arreglo" que intentábamos, el número de casos límite se multiplicaba. En muchos casos, descubrimos que incluso pequeños cambios en el algoritmo de obtención fallaban en escenarios extraños que antes funcionaban.
COMIENZO FRESCO
Todos estos problemas nos llevaron a una conclusión ineludible: Necesitábamos un cambio drástico en nuestro enfoque. Examinando el problema, nos dimos cuenta de que teníamos muchas suposiciones sobre la forma en que debía funcionar el algoritmo fetch que nos ponían las cosas más difíciles.
Una de esas suposiciones era que el algoritmo de búsqueda siempre debía evitar solicitar segmentos que ya estuvieran almacenados en el búfer. El problema es que es muy difícil razonar sobre el estado del búfer una vez que se combinan los efectos de la búsqueda, la recolección de basura del búfer (algo que MSE hace automáticamente entre bastidores) y las fuentes que naturalmente introducen huecos. Al final, esto significaba que nuestro algoritmo dependía inextricablemente del búfer siempre cambiante de MSE.
El nuevo fetcher elimina ésta y muchas otras suposiciones para simplificar las cosas al máximo. Por ejemplo, ahora el reproductor limpia el búfer después de cada búsqueda para que sea más fácil razonar sobre el estado del búfer y no intente evitar cargar un segmento que ya está presente en el búfer.
RECORRIENDO EL CAMINO
Tras reexaminar nuestros supuestos, nos dimos cuenta de que acertar el 100% de las veces es imposible, pero acertar de forma conservadora el 100% de las veces es totalmente posible. Una suposición conservadora es la que corresponde al segmento anterior o igual al segmento deseado. Hacer un cálculo conservador significa que siempre se puede encontrar el segmento correcto simplemente avanzando por los segmentos de una lista de reproducción.
Entendiendo esto, cambiamos drásticamente la naturaleza del problema. Ahora, siempre estamos buscando regiones contiguas después de hacer una suposición inicial. Eso significa que los detalles sobre el estado del búfer -los huecos- ya no nos preocupan, puesto que, por definición, son intrínsecos a los medios y no se deben al comportamiento del algoritmo de obtención.
La única cuestión que quedaba por resolver era cómo correlacionar segmentos y tiempos entre variantes en una lista de reproducción en directo. Para ello, introducimos el concepto de "punto de sincronización". Un punto de sincronización se define como una correspondencia conocida entre un índice de segmento y un tiempo de visualización, es decir, el tiempo que se obtiene al llamar a player.currentTime()
.
El nuevo algoritmo de búsqueda sólo tiene tres modos de funcionamiento:
- Adivinar de forma conservadora de qué segmento empezar a descargar
- Simplemente avanzando por la lista de reproducción
- Intentar crear un punto de sincronización
Este último estado sólo se produce cuando el algoritmo de búsqueda no puede utilizar la información guardada para hacer una estimación. Es poco frecuente, pero cuando eso ocurre, debemos descargar un segmento -cualquier segmento- y utilizar las marcas de tiempo internas de los medios para generar un "punto de sincronización" que el algoritmo de obtención pueda utilizar para hacer una estimación conservadora antes de seguir adelante.
El resultado final de estos cambios es una mejor experiencia de reproducción HLS. En particular, la reproducción en directo debería iniciarse más rápidamente y reproducirse con mayor fiabilidad.