Post Mortem: La Sopa de Videos

rzerostern

Marco Ramírez

Posted on October 26, 2020

Post Mortem: La Sopa de Videos

Hola ¿cómo están, amigos de DEV.TO?

Pues bien, este es mi primer post que escribo para esta plataforma, sin embargo, primero permítanme presentarme.

Mi nombre es Marco Ramírez y soy un Desarrollador Android con 4 años de experiencia del Estado de México (concretamente de Tlalnepantla). He trabajado en sinfín de proyectos con Java y Kotlin para varios clientes. También he trabajado con Flutter para desarrollo de apps para Android y iOS (mejor conocidas como apps híbridas).

Me encanta la 🍺 y disfruto de hacer videos para mi canal de YouTube (espero retomarlo pronto, pues el trabajo se ha incrementado). Pueden suscribirse a mi canal y ver mis streamings así como futuros videos sobre ingeniería de software.

Muy bien, suficiente de presentaciones, vamos al grano.

Recientemente, me ha tocado armar una aplicación de video bajo demanda (VoD por sus siglas en inglés). Esta app la armé para mostrar las capacidades de un servidor multimedia: ANT Media Server y me encantaría contarles cómo me fue :D.

¿Una app VOD?

Empecemos por definir una app VOD a nivel técnico.

Una app de este tipo lo que hace es distribuir contenido en video bajo demanda del usuario por medio de un servidor multimedia que aloja el contenido en el mismo y es distribuido por diversos protocolos. El más conocido es el HLS que tiene compatibilidad con la gran mayoría de los dispositivos de hoy en día.

La app VOD se encarga de recibir dicho contenido como un request común y corriente y lo distribuye a un reproductor de video integrado a la aplicación. En estos casos, los reproductores podemos encontrarlos en Web (con Javascript) y en componentes nativos de Android y iOS.

¿Qué nos encontramos?

Primeramente, necesitamos tener en cuenta varias cosas con ANT Media Server.

ANT es bastante amigable desde el concepto en que su licencia comunitaria es completamente gratis y tiene muchísimas bondades para poder armar plataformas para streaming incluída la transmisión vía RTMP. Igualmente si así se desea el streaming se puede dejar para grabar en el disco del servidor.

Igualmente, al ser comunitario el server, tiene sus propios scripts para que puedas poner tus certificados SSL sin problemas con Certbot y así transportar tus contenidos por HTTPS.

Hay otras cualidades como el compartir tu streaming en Facebook y YouTube, sin embargo estas ya son de paga, pero por $69 USD al mes o $ 1400 USD de forma permanente, siento que vale la pena si es que quieres tener tu propio sistema de streaming.

¿Cómo puedo implementarlo?

Por tiempo, ahorita tuve que ocupar una imagen de DigitalOcean que tenía el software precargado y ya todo dispuesto para poder servir la aplicación inmediatamente. Usé una droplet de $ 5 USD con Ubuntu 20.04 LTS y hasta ahorita me ha corrido sin ningún problema. De hecho puedo mostrar las gráficas de estos últimos días:

CPU
Gráfica de CPU

Ancho de Banda
Gráfica de CPU

Esto ha sido probado con muy poquitos usuarios. Por lo que se debe de tomar en cuenta como en cualquier app el número estimado de usuarios que vayan a consumirla, pues en base a ello tenemos que poner la infraestructura :3.

Igualmente, les dejo en este enlace un tutorial para poder instalarlo desde cualquier servidor con Ubuntu.

Ahora, para comunicar el servidor multimedia con la app, usé Firebase para poder intercomunicar los datos de cada uno de los videos con la app. Dentro de este tenemos el Realtime Database donde pude montar el "esquema de base de datos". La estructura es más o menos así:

movies
  |
  |- 0
     |- moviedb_id: Integer (ID de MovieDB)
     |- name: String (nombre del video)
     |- poster_uri: String
     |- protagonists: ArrayList<String> (se declaran dentro de este nodo)
     |- uri: String (URL del video)

Enter fullscreen mode Exit fullscreen mode

Ya con esto y al implementar los servicios de Firebase en mi app, puedo usar el siguiente código para poder llamar a los datos:

private fun fetchMovies() {
        val database = Firebase.database
        val myRef = database.getReference("movies")

        movieItems = ArrayList()

        myRef.addValueEventListener(object: ValueEventListener{
            override fun onCancelled(error: DatabaseError) {
                Log.w("App", "Failed to read value.", error.toException())
            }

            override fun onDataChange(snapshot: DataSnapshot) {
                for(item: DataSnapshot in snapshot.children) {
                    var protagonists = ArrayList<String>();

                    for(protagonistItem in item.child("protagonists").children) {
                        protagonists.add(protagonistItem.value.toString())
                    }

                    val movieItem = MovieItem(item.child("moviedb_id").value.toString(), item.child("name").value.toString(),
                        item.child("uri").value.toString(),
                        item.child("poster_uri").value.toString(), protagonists)

                    movieItems.add(movieItem)
                }

                initRecyclerView(movieItems)
            }

        })
    }
Enter fullscreen mode Exit fullscreen mode

Ahora en la app ¿qué nos encontramos?

El MediaController

Primeramente, nos encontramos que en Android usamos el componente VideoView para poder reproducir el video que nosotros le entreguemos. Pero lo primero que nos encontramos es que el VideoView carece de cualquier control. No es como en JS que ya tiene todo en el mismo contenedor, por lo que tienes que ponerle un MediaController.

El MediaController es quien se va a encargar de poner los clásicos botones de Play, Rewind, FF, etc y controlar el streaming. El código para implementar en Kotlin es el siguiente:

 val mediaController = MediaController(requireContext(), true)
 mediaController.setAnchorView(videoView)

 videoView.setOnPreparedListener(OnPreparedListener { mediaController.show(0) })

 videoView.setVideoPath(arguments?.getString("uri"))
 videoView.setMediaController(mediaController)
 videoView.start()
Enter fullscreen mode Exit fullscreen mode

Al implementar este snippet ya podremos controlar nuestros controles en el VideoView y poder manejar la reproducción del video.

De poner en Portrait la pantalla y otras dificultades...

Otro tema que podemos llegar a encontrarnos es el poner la interfaz en landscape y regresarla a portrait al salir del reproductor. Me estuve quebrando un poco la cabeza pues al usar Navigation Controllers, toda la navegación se hace desde una sola Activity, sin embargo es algo que no se pueda solucionar.

Dentro del onCreateView haz lo siguiente:

override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
        requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)

requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
        requireActivity().window.statusBarColor = Color.TRANSPARENT;
        return inflater.inflate(R.layout.fragment_video_player, container, false)
    }
Enter fullscreen mode Exit fullscreen mode

Igualmente en el fragmento anterior a este, debes implementar en el onResume el siguiente código para regresar a modo Portrait:

override fun onResume() {
        super.onResume()
        val activity = requireActivity() as AppCompatActivity

        activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
        activity.supportActionBar!!.show()

        activity.window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
        activity.window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS)
        activity.window.clearFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
        activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)

        activity.window.statusBarColor = resources.getColor(R.color.colorPrimaryDark)

        activity.window.setFlags(
            WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN,
            WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
        activity.window.decorView.systemUiVisibility = 0
    }
Enter fullscreen mode Exit fullscreen mode

Ya con esto, se hizo un workflow un poco más estable. La navegabilidad básica ya es historia contada en muchos posts jejejeje (Adapters, RecyclerViews, Navigation Controllers, REST APIs, etc). Próximamente (si el trabajo y mis actividades lo permiten) les traeré más contenido.

¿Qué sigue?

Ahorita la verdad el código lo hice muy rápido por lo que me gustaría hacer un poco más amigable al desarrollador el mismo y ya montarle inclusive MVVM y otras buenas prácticas que por tiempo no pude montar así como:

  • Compatibilidad con Chromecast.
  • Detectar el punto de interrupción.
  • Prescindir de Firebase y usar una REST API decente.
  • Medidas de seguridad y antipiratería.

Finalmente, quisiera invitarlos a suscribirse a mi canal de YouTube: The Dev Gang donde pueden encontrar un poquito de contenido que he hecho en tiempo libre esperando que este pueda servirles en algún proyecto.

Happy Coding !!!

P.D: Lamentablemente no pude poner screenshots de la app por motivos de confidencialidad, pero próximamente puedo hacer incluso un release más estable.

💖 💪 🙅 🚩
rzerostern
Marco Ramírez

Posted on October 26, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Post Mortem: La Sopa de Videos
android Post Mortem: La Sopa de Videos

October 26, 2020